All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldif.LDIFModify Maven / Gradle / Ivy

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

The newest version!
/*
 * Copyright 2020-2024 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2020-2024 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2020-2024 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldif;



import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPOutputStream;

import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.ChangeType;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.InternalSDKHelper;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.Version;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
import com.unboundid.util.CommandLineTool;
import com.unboundid.util.Debug;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.PassphraseEncryptedOutputStream;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.Validator;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.IntegerArgument;

import static com.unboundid.ldif.LDIFMessages.*;



/**
 * This class provides a command-line tool that can be used to apply a set of
 * changes to data in an LDIF file.
 */
@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
public final class LDIFModify
       extends CommandLineTool
{
  /**
   * The server root directory for the Ping Identity Directory Server (or
   * related Ping Identity server product) that contains this tool, if
   * applicable.
   */
  @NotNull private static final File PING_SERVER_ROOT =
       InternalSDKHelper.getPingIdentityServerRoot();



  /**
   * Indicates whether the tool is running as part of a Ping Identity Directory
   * Server (or related Ping Identity Server Product) installation.
   */
  private static final boolean PING_SERVER_AVAILABLE =
       (PING_SERVER_ROOT != null);



  /**
   * The column at which to wrap long lines.
   */
  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;



  // The completion message for this tool.
  @NotNull private final AtomicReference completionMessage;

  // Encryption passphrases used thus far.
  @NotNull private final List inputEncryptionPassphrases;

  // The command-line arguments supported by this tool.
  @Nullable private BooleanArgument compressTarget;
  @Nullable private BooleanArgument doNotWrap;
  @Nullable private BooleanArgument encryptTarget;
  @Nullable private BooleanArgument ignoreDeletesOfNonexistentEntries;
  @Nullable private BooleanArgument ignoreDuplicateDeletes;
  @Nullable private BooleanArgument ignoreModifiesOfNonexistentEntries;
  @Nullable private BooleanArgument lenientModifications;
  @Nullable private BooleanArgument strictModifications;
  @Nullable private BooleanArgument noSchemaCheck;
  @Nullable private BooleanArgument stripTrailingSpaces;
  @Nullable private BooleanArgument suppressComments;
  @Nullable private FileArgument changesEncryptionPassphraseFile;
  @Nullable private FileArgument changesLDIF;
  @Nullable private FileArgument sourceEncryptionPassphraseFile;
  @Nullable private FileArgument sourceLDIF;
  @Nullable private FileArgument targetEncryptionPassphraseFile;
  @Nullable private FileArgument targetLDIF;
  @Nullable private IntegerArgument wrapColumn;

  // Variables that may be used by support for a legacy implementation.
  @Nullable private LDIFReader changesReader;
  @Nullable private LDIFReader sourceReader;
  @Nullable private LDIFWriter targetWriter;
  @Nullable private List errorMessages;



  /**
   * Invokes this tool with the provided set of command-line arguments.
   *
   * @param  args  The set of arguments provided to this tool.  It may be
   *               empty but must not be {@code null}.
   */
  public static void main(@NotNull final String... args)
  {
    final ResultCode resultCode = main(System.out, System.err, args);
    if (resultCode != ResultCode.SUCCESS)
    {
      System.exit(resultCode.intValue());
    }
  }



  /**
   * Invokes this tool with the provided set of command-line arguments, using
   * the given output and error streams.
   *
   * @param  out   The output stream to use for standard output.  It may be
   *               {@code null} if standard output should be suppressed.
   * @param  err   The output stream to use for standard error.  It may be
   *               {@code null} if standard error should be suppressed.
   * @param  args  The set of arguments provided to this tool.  It may be
   *               empty but must not be {@code null}.
   *
   * @return  A result code indicating the status of processing.  Any result
   *          code other than {@link ResultCode#SUCCESS} should be considered
   *          an error.
   */
  @NotNull()
  public static ResultCode main(@Nullable final OutputStream out,
                                @Nullable final OutputStream err,
                                @NotNull final String... args)
  {
    final LDIFModify tool = new LDIFModify(out, err);
    return tool.runTool(args);
  }



  /**
   * Invokes this tool with the provided readers and writer.  This method is
   * primarily intended for legacy backward compatibility with the Ping Identity
   * Directory Server and does not provide access to all functionality offered
   * by this tool.
   *
   * @param  sourceReader   An LDIF reader that may be used to read the entries
   *                        to be updated.  It must not be {@code null}.  Note
   *                        this the reader will be closed when the tool
   *                        completes.
   * @param  changesReader  An LDIF reader that may be used to read the changes
   *                        to apply.  It must not be {@code null}.  Note that
   *                        this reader will be closed when the tool completes.
   * @param  targetWriter   An LDIF writer that may be used to write the updated
   *                        entries.  It must not be {@code null}.  Note that
   *                        this writer will be closed when the tool completes.
   * @param  errorMessages  A list that will be updated with any errors
   *                        encountered during processing.  It must not be
   *                        {@code null} and must be updatable.
   *
   * @return  {@code true} if processing completed successfully, or
   *          {@code false} if one or more errors were encountered.
   */
  public static boolean main(@NotNull final LDIFReader sourceReader,
                             @NotNull final LDIFReader changesReader,
                             @NotNull final LDIFWriter targetWriter,
                             @NotNull final List errorMessages)
  {
    Validator.ensureNotNull(sourceReader, changesReader, targetWriter,
         errorMessages);

    final LDIFModify tool = new LDIFModify(null, null);
    tool.sourceReader = sourceReader;
    tool.changesReader = changesReader;
    tool.targetWriter = targetWriter;
    tool.errorMessages = errorMessages;

    try
    {
      final ResultCode resultCode =
           tool.runTool("--suppressComments", "--lenientModifications");
      return (resultCode == ResultCode.SUCCESS);
    }
    finally
    {
      try
      {
        sourceReader.close();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }

      try
      {
        changesReader.close();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }

      try
      {
        targetWriter.close();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }
  }



  /**
   * Creates a new instance of this tool with the provided output and error
   * streams.
   *
   * @param  out  The output stream to use for standard output.  It may be
   *              {@code null} if standard output should be suppressed.
   * @param  err  The output stream to use for standard error.  It may be
   *              {@code null} if standard error should be suppressed.
   */
  public LDIFModify(@Nullable final OutputStream out,
                    @Nullable final OutputStream err)
  {
    super(out, err);

    completionMessage = new AtomicReference<>();
    inputEncryptionPassphrases = new ArrayList<>(5);

    compressTarget = null;
    doNotWrap = null;
    encryptTarget = null;
    ignoreDeletesOfNonexistentEntries = null;
    ignoreDuplicateDeletes = null;
    ignoreModifiesOfNonexistentEntries = null;
    lenientModifications = null;
    noSchemaCheck = null;
    strictModifications = null;
    stripTrailingSpaces = null;
    suppressComments = null;
    changesEncryptionPassphraseFile = null;
    changesLDIF = null;
    sourceEncryptionPassphraseFile = null;
    sourceLDIF = null;
    targetEncryptionPassphraseFile = null;
    targetLDIF = null;
    wrapColumn = null;

    changesReader = null;
    sourceReader = null;
    targetWriter = null;
    errorMessages = null;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public String getToolName()
  {
    return "ldifmodify";
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public String getToolDescription()
  {
    return INFO_LDIFMODIFY_TOOL_DESCRIPTION.get();
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public List getAdditionalDescriptionParagraphs()
  {
    return Arrays.asList(
         INFO_LDIFMODIFY_TOOL_DESCRIPTION_2.get(),
         INFO_LDIFMODIFY_TOOL_DESCRIPTION_3.get(),
         INFO_LDIFMODIFY_TOOL_DESCRIPTION_4.get(),
         INFO_LDIFMODIFY_TOOL_DESCRIPTION_5.get());
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public String getToolVersion()
  {
    return Version.NUMERIC_VERSION_STRING;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean supportsInteractiveMode()
  {
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean defaultsToInteractiveMode()
  {
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean supportsPropertiesFile()
  {
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  protected boolean supportsDebugLogging()
  {
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @Nullable()
  protected String getToolCompletionMessage()
  {
    return completionMessage.get();
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public void addToolArguments(@NotNull final ArgumentParser parser)
         throws ArgumentException
  {
    sourceLDIF = new FileArgument('s', "sourceLDIF", (sourceReader == null), 1,
         null, INFO_LDIFMODIFY_ARG_DESC_SOURCE_LDIF.get(), true, true, true,
         false);
    sourceLDIF.addLongIdentifier("source-ldif", true);
    sourceLDIF.addLongIdentifier("sourceFile", true);
    sourceLDIF.addLongIdentifier("source-file", true);
    sourceLDIF.addLongIdentifier("source", true);
    sourceLDIF.addLongIdentifier("inputLDIF", true);
    sourceLDIF.addLongIdentifier("input-ldif", true);
    sourceLDIF.addLongIdentifier("inputFile", true);
    sourceLDIF.addLongIdentifier("input-file", true);
    sourceLDIF.addLongIdentifier("input", true);
    sourceLDIF.addLongIdentifier("ldifFile", true);
    sourceLDIF.addLongIdentifier("ldif-file", true);
    sourceLDIF.addLongIdentifier("ldif", true);
    sourceLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(sourceLDIF);


    final String sourcePWDesc;
    if (PING_SERVER_AVAILABLE)
    {
      sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_PING_SERVER.get();
    }
    else
    {
      sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_STANDALONE.get();
    }
    sourceEncryptionPassphraseFile = new FileArgument(null,
         "sourceEncryptionPassphraseFile", false, 1, null, sourcePWDesc, true,
         true, true, false);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "source-encryption-passphrase-file", true);
    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePassphraseFile",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier("source-passphrase-file",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "sourceEncryptionPasswordFile", true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "source-encryption-password-file", true);
    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePasswordFile",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier("source-password-file",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "inputEncryptionPassphraseFile", true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "input-encryption-passphrase-file", true);
    sourceEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file",
         true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "inputEncryptionPasswordFile", true);
    sourceEncryptionPassphraseFile.addLongIdentifier(
         "input-encryption-password-file", true);
    sourceEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true);
    sourceEncryptionPassphraseFile.addLongIdentifier("input-password-file",
         true);
    sourceEncryptionPassphraseFile.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(sourceEncryptionPassphraseFile);


    changesLDIF = new FileArgument('m', "changesLDIF", (changesReader == null),
         1, null, INFO_LDIFMODIFY_ARG_DESC_CHANGES_LDIF.get(), true, true, true,
         false);
    changesLDIF.addLongIdentifier("changes-ldif", true);
    changesLDIF.addLongIdentifier("changesFile", true);
    changesLDIF.addLongIdentifier("changes-file", true);
    changesLDIF.addLongIdentifier("changes", true);
    changesLDIF.addLongIdentifier("updatesLDIF", true);
    changesLDIF.addLongIdentifier("updates-ldif", true);
    changesLDIF.addLongIdentifier("updatesFile", true);
    changesLDIF.addLongIdentifier("updates-file", true);
    changesLDIF.addLongIdentifier("updates", true);
    changesLDIF.addLongIdentifier("modificationsLDIF", true);
    changesLDIF.addLongIdentifier("modifications-ldif", true);
    changesLDIF.addLongIdentifier("modificationsFile", true);
    changesLDIF.addLongIdentifier("modifications-file", true);
    changesLDIF.addLongIdentifier("modifications", true);
    changesLDIF.addLongIdentifier("modsLDIF", true);
    changesLDIF.addLongIdentifier("mods-ldif", true);
    changesLDIF.addLongIdentifier("modsFile", true);
    changesLDIF.addLongIdentifier("mods-file", true);
    changesLDIF.addLongIdentifier("mods", true);
    changesLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(changesLDIF);


    final String changesPWDesc;
    if (PING_SERVER_AVAILABLE)
    {
      changesPWDesc =
           INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_PING_SERVER.get();
    }
    else
    {
      changesPWDesc = INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_STANDALONE.get();
    }
    changesEncryptionPassphraseFile = new FileArgument(null,
         "changesEncryptionPassphraseFile", false, 1, null, changesPWDesc, true,
         true, true, false);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "changes-encryption-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier("changesPassphraseFile",
         true);
    changesEncryptionPassphraseFile.addLongIdentifier("changes-passphrase-file",
         true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "changesEncryptionPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "changes-encryption-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier("changesPasswordFile",
         true);
    changesEncryptionPassphraseFile.addLongIdentifier("changes-password-file",
         true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updatesEncryptionPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updates-encryption-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updatesPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updates-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updatesEncryptionPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updates-encryption-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updatesPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "updates-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modificationsEncryptionPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modifications-encryption-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modificationsPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modifications-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modificationsEncryptionPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modifications-encryption-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modificationsPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modifications-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modsEncryptionPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "mods-encryption-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modsPassphraseFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "mods-passphrase-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modsEncryptionPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "mods-encryption-password-file", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "modsPasswordFile", true);
    changesEncryptionPassphraseFile.addLongIdentifier(
         "mods-password-file", true);
    changesEncryptionPassphraseFile.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(changesEncryptionPassphraseFile);


    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
         INFO_LDIFMODIFY_ARG_DESC_STRIP_TRAILING_SPACES.get());
    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
    stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true);
    stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true);
    stripTrailingSpaces.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(stripTrailingSpaces);


    lenientModifications = new BooleanArgument(null, "lenientModifications", 1,
         INFO_LDIFMODIFY_ARG_DESC_LENIENT_MODIFICATIONS.get());
    lenientModifications.addLongIdentifier("lenient-modifications", true);
    lenientModifications.addLongIdentifier("lenientModification", true);
    lenientModifications.addLongIdentifier("lenient-modification", true);
    lenientModifications.addLongIdentifier("lenientMods", true);
    lenientModifications.addLongIdentifier("lenient-mods", true);
    lenientModifications.addLongIdentifier("lenientMod", true);
    lenientModifications.addLongIdentifier("lenient-mod", true);
    lenientModifications.addLongIdentifier("lenient", true);
    lenientModifications.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    lenientModifications.setHidden(true);
    parser.addArgument(lenientModifications);


    strictModifications = new BooleanArgument(null, "strictModifications", 1,
         INFO_LDIFMODIFY_ARG_DESC_STRICT_MODIFICATIONS.get());
    strictModifications.addLongIdentifier("strict-modifications", true);
    strictModifications.addLongIdentifier("strictModification", true);
    strictModifications.addLongIdentifier("strict-modification", true);
    strictModifications.addLongIdentifier("strictMods", true);
    strictModifications.addLongIdentifier("strict-mods", true);
    strictModifications.addLongIdentifier("strictMod", true);
    strictModifications.addLongIdentifier("strict-mod", true);
    strictModifications.addLongIdentifier("strict", true);
    strictModifications.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(strictModifications);


    ignoreDuplicateDeletes = new BooleanArgument(null, "ignoreDuplicateDeletes",
         1, INFO_LDIFMODIFY_ARG_DESC_IGNORE_DUPLICATE_DELETES.get());
    ignoreDuplicateDeletes.addLongIdentifier("ignore-duplicate-deletes", true);
    ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatedDeletes", true);
    ignoreDuplicateDeletes.addLongIdentifier("ignore-repeated-deletes", true);
    ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatDeletes", true);
    ignoreDuplicateDeletes.addLongIdentifier("ignore-repeat-deletes", true);
    ignoreDuplicateDeletes.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(ignoreDuplicateDeletes);


    ignoreDeletesOfNonexistentEntries = new BooleanArgument(null,
         "ignoreDeletesOfNonexistentEntries", 1,
         INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_DELETES.get());
    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
         "ignore-deletes-of-nonexistent-entries", true);
    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
         "ignoreNonexistentDeletes", true);
    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
         "ignore-nonexistent-deletes", true);
    ignoreDeletesOfNonexistentEntries.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(ignoreDeletesOfNonexistentEntries);


    ignoreModifiesOfNonexistentEntries = new BooleanArgument(null,
         "ignoreModifiesOfNonexistentEntries", 1,
         INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_MODIFIES.get());
    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
         "ignore-modifies-of-nonexistent-entries", true);
    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
         "ignoreNonexistentModifies", true);
    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
         "ignore-nonexistent-modifies", true);
    ignoreModifiesOfNonexistentEntries.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
    parser.addArgument(ignoreModifiesOfNonexistentEntries);


    targetLDIF = new FileArgument('t', "targetLDIF", (targetWriter == null), 1,
         null, INFO_LDIFMODIFY_ARG_DESC_TARGET_LDIF.get(), false, true, true,
         false);
    targetLDIF.addLongIdentifier("target-ldif", true);
    targetLDIF.addLongIdentifier("targetFile", true);
    targetLDIF.addLongIdentifier("target-file", true);
    targetLDIF.addLongIdentifier("target", true);
    targetLDIF.addLongIdentifier("outputLDIF", true);
    targetLDIF.addLongIdentifier("output-ldif", true);
    targetLDIF.addLongIdentifier("outputFile", true);
    targetLDIF.addLongIdentifier("output-file", true);
    targetLDIF.addLongIdentifier("output", true);
    targetLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(targetLDIF);


    compressTarget = new BooleanArgument(null, "compressTarget", 1,
         INFO_LDIFMODIFY_ARG_DESC_COMPRESS_TARGET.get());
    compressTarget.addLongIdentifier("compress-target", true);
    compressTarget.addLongIdentifier("compressOutput", true);
    compressTarget.addLongIdentifier("compress-output", true);
    compressTarget.addLongIdentifier("compress", true);
    compressTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(compressTarget);


    encryptTarget = new BooleanArgument(null, "encryptTarget", 1,
         INFO_LDIFMODIFY_ARG_DESC_ENCRYPT_TARGET.get());
    encryptTarget.addLongIdentifier("encrypt-target", true);
    encryptTarget.addLongIdentifier("encryptOutput", true);
    encryptTarget.addLongIdentifier("encrypt-output", true);
    encryptTarget.addLongIdentifier("encrypt", true);
    encryptTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(encryptTarget);


    targetEncryptionPassphraseFile = new FileArgument(null,
         "targetEncryptionPassphraseFile", false, 1, null,
         INFO_LDIFMODIFY_ARG_DESC_TARGET_PW_FILE.get(), true, true, true,
         false);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "target-encryption-passphrase-file", true);
    targetEncryptionPassphraseFile.addLongIdentifier("targetPassphraseFile",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier("target-passphrase-file",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "targetEncryptionPasswordFile", true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "target-encryption-password-file", true);
    targetEncryptionPassphraseFile.addLongIdentifier("targetPasswordFile",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier("target-password-file",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "outputEncryptionPassphraseFile", true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "output-encryption-passphrase-file", true);
    targetEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "outputEncryptionPasswordFile", true);
    targetEncryptionPassphraseFile.addLongIdentifier(
         "output-encryption-password-file", true);
    targetEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
         true);
    targetEncryptionPassphraseFile.addLongIdentifier("output-password-file",
         true);
    targetEncryptionPassphraseFile.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());

    parser.addArgument(targetEncryptionPassphraseFile);


    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
         INFO_LDIFMODIFY_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE);
    wrapColumn.addLongIdentifier("wrap-column", true);
    wrapColumn.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(wrapColumn);


    doNotWrap = new BooleanArgument('T', "doNotWrap", 1,
         INFO_LDIFMODIFY_ARG_DESC_DO_NOT_WRAP.get());
    doNotWrap.addLongIdentifier("do-not-wrap", true);
    doNotWrap.addLongIdentifier("dontWrap", true);
    doNotWrap.addLongIdentifier("dont-wrap", true);
    doNotWrap.addLongIdentifier("noWrap", true);
    doNotWrap.addLongIdentifier("no-wrap", true);
    doNotWrap.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(doNotWrap);


    suppressComments = new BooleanArgument(null, "suppressComments", 1,
         INFO_LDIFMODIFY_ARG_DESC_SUPPRESS_COMMENTS.get());
    suppressComments.addLongIdentifier("suppress-comments", true);
    suppressComments.addLongIdentifier("excludeComments", true);
    suppressComments.addLongIdentifier("exclude-comments", true);
    suppressComments.addLongIdentifier("noComments", true);
    suppressComments.addLongIdentifier("no-comments", true);
    suppressComments.setArgumentGroupName(
         INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
    parser.addArgument(suppressComments);


    noSchemaCheck = new BooleanArgument(null, "noSchemaCheck", 1,
         INFO_LDIFMODIFY_ARG_DESC_NO_SCHEMA_CHECK.get());
    noSchemaCheck.addLongIdentifier("no-schema-check", true);
    noSchemaCheck.setHidden(true);
    parser.addArgument(noSchemaCheck);


    parser.addExclusiveArgumentSet(lenientModifications, strictModifications);

    parser.addExclusiveArgumentSet(wrapColumn, doNotWrap);

    parser.addDependentArgumentSet(targetEncryptionPassphraseFile,
         encryptTarget);
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public ResultCode doToolProcessing()
  {
    // Read all of the changes into memory.
    final Map> addAndSubsequentChangeRecords =
         new TreeMap<>();
    final Map deletedEntryDNs = new TreeMap<>();
    final Map> modifyChangeRecords =
         new HashMap<>();
    final Map>>
         modifyDNAndSubsequentChangeRecords = new TreeMap<>();
    final AtomicReference resultCode = new AtomicReference<>();
    try
    {
      readChangeRecords(addAndSubsequentChangeRecords, deletedEntryDNs,
           modifyChangeRecords, modifyDNAndSubsequentChangeRecords, resultCode);
    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      logCompletionMessage(true, e.getMessage());
      resultCode.compareAndSet(null, e.getResultCode());
      return resultCode.get();
    }


    boolean changesIgnored = false;
    LDIFReader ldifReader = null;
    LDIFWriter ldifWriter = null;
    final AtomicLong entriesRead = new AtomicLong(0L);
    final AtomicLong entriesUpdated = new AtomicLong(0L);
    try
    {
      // Open the source LDIF file for reading.
      try
      {
        ldifReader = getLDIFReader(sourceReader, sourceLDIF.getValue(),
             sourceEncryptionPassphraseFile.getValue());
      }
      catch (final LDAPException e)
      {
        Debug.debugException(e);
        logCompletionMessage(true, e.getMessage());
        return e.getResultCode();
      }


      // Open the target LDIF file for writing.
      try
      {
        ldifWriter = getLDIFWriter(targetWriter);
      }
      catch (final LDAPException e)
      {
        Debug.debugException(e);
        logCompletionMessage(true, e.getMessage());
        return e.getResultCode();
      }


      // Iterate through the source LDIF file and apply changes as appropriate.
      final StringBuilder comment = new StringBuilder();
      while (true)
      {
        final LDIFRecord sourceRecord;
        try
        {
          sourceRecord = ldifReader.readLDIFRecord();
        }
        catch (final LDIFException e)
        {
          Debug.debugException(e);

          if (e.mayContinueReading())
          {
            resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
            wrapErr(ERR_LDIFMODIFY_RECOVERABLE_DECODE_ERROR.get(
                 sourceLDIF.getValue(), StaticUtils.getExceptionMessage(e)));
            continue;
          }
          else
          {
            logCompletionMessage(true,
                 ERR_LDIFMODIFY_UNRECOVERABLE_DECODE_ERROR.get(
                      sourceLDIF.getValue(),
                      StaticUtils.getExceptionMessage(e)));
            return ResultCode.DECODING_ERROR;
          }
        }
        catch (final IOException e)
        {
          Debug.debugException(e);
          logCompletionMessage(true,
               ERR_LDIFMODIFY_READ_ERROR.get(sourceLDIF.getValue(),
                    StaticUtils.getExceptionMessage(e)));
          return ResultCode.LOCAL_ERROR;
        }


        // If the record we read was null, then we've hit the end of the source
        // content.
        if (sourceRecord == null)
        {
          break;
        }


        // If the record we read was an entry, then apply changes to it.  If it
        // was not, then that's an error.
        comment.setLength(0);

        final LDIFRecord targetRecord;
        if (sourceRecord instanceof Entry)
        {
          entriesRead.incrementAndGet();
          targetRecord = updateEntry((Entry) sourceRecord,
               addAndSubsequentChangeRecords, deletedEntryDNs,
               modifyChangeRecords, modifyDNAndSubsequentChangeRecords, comment,
               resultCode, entriesUpdated);
        }
        else
        {
          targetRecord = sourceRecord;
          // NOTE:  We're using false for the isError flag in this case because
          // a better error will be recorded by the createChangeRecordComment
          // call below.
          appendComment(comment,
               ERR_LDIFMODIFY_COMMENT_SOURCE_RECORD_NOT_ENTRY.get(), false);

          final StringBuilder msgBuffer = new StringBuilder();
          createChangeRecordComment(msgBuffer,
               ERR_LDIFMODIFY_OUTPUT_SOURCE_RECORD_NOT_ENTRY.get(
                    sourceLDIF.getValue().getAbsolutePath()),
               sourceRecord, true);
          wrapErr(msgBuffer.toString());
          resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
        }


        // Write the potentially updated entry to the target LDIF file.  If the
        // target record is null, then that means the entry has been deleted,
        // but we still may want to write a comment about the deleted entry to
        // the target file.
        try
        {
          if (targetRecord == null)
          {
            if ((comment.length() > 0) && (! suppressComments.isPresent()))
            {
              writeLDIFComment(ldifWriter, comment, false);
            }
          }
          else
          {
            writeLDIFRecord(ldifWriter, targetRecord, comment);
          }
        }
        catch (final IOException e)
        {
          Debug.debugException(e);
          logCompletionMessage(true,
               ERR_LDIFMODIFY_WRITE_ERROR.get(targetLDIF.getValue(),
                    StaticUtils.getExceptionMessage(e)));
          return ResultCode.LOCAL_ERROR;
        }
      }


      try
      {
        // If there are any remaining add records, then process them.
        final AtomicBoolean isUpdated = new AtomicBoolean();
        for (final List records :
             addAndSubsequentChangeRecords.values())
        {
          final Iterator iterator = records.iterator();
          final LDIFAddChangeRecord addChangeRecord =
               (LDIFAddChangeRecord) iterator.next();
          Entry entry = addChangeRecord.getEntryToAdd();
          comment.setLength(0);
          if (iterator.hasNext())
          {
            createChangeRecordComment(comment,
                 INFO_LDIFMODIFY_ADDING_ENTRY_WITH_MODS.get(), addChangeRecord,
                 false);
            while (iterator.hasNext())
            {
              entry = applyModification(entry,
                   (LDIFModifyChangeRecord) iterator.next(), isUpdated,
                   resultCode, comment);
            }
          }
          else
          {
            appendComment(comment,
                 INFO_LDIFMODIFY_ADDING_ENTRY_NO_MODS.get(), false);
          }

          writeLDIFRecord(ldifWriter, entry, comment);
          entriesUpdated.incrementAndGet();
        }


        // If there are any remaining DNs to delete, then those entries must not
        // have been in the source LDIF.
        for (final Map.Entry e : deletedEntryDNs.entrySet())
        {
          if (e.getValue() == Boolean.FALSE)
          {
            if (ignoreDeletesOfNonexistentEntries.isPresent())
            {
              changesIgnored = true;
            }
            else
            {
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              writeLDIFComment(ldifWriter,
                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_DELETE.get(
                        e.getKey().toString()),
                   true);
            }
          }
        }


        // If there are any remaining modify change records, then those entries
        // must not have been in the source LDIF.
        for (final List l :
             modifyChangeRecords.values())
        {
          for (final LDIFChangeRecord r : l)
          {
            if (ignoreModifiesOfNonexistentEntries.isPresent())
            {
              changesIgnored = true;
            }
            else
            {
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              comment.setLength(0);
              createChangeRecordComment(comment,
                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true);
              writeLDIFComment(ldifWriter, comment, false);
            }
          }
        }


        // If there are any remaining modify DN change records, then those
        // entries must not have been in the source LDIF.
        for (final ObjectPair> l :
             modifyDNAndSubsequentChangeRecords.values())
        {
          for (final LDIFChangeRecord r : l.getSecond())
          {
            resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
            comment.setLength(0);
            if (r instanceof LDIFModifyDNChangeRecord)
            {
              createChangeRecordComment(comment,
                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_RENAME.get(), r, true);
            }
            else
            {
              createChangeRecordComment(comment,
                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true);
            }
            writeLDIFComment(ldifWriter, comment, false);
          }
        }
      }
      catch (final IOException e)
      {
        Debug.debugException(e);
        logCompletionMessage(true,
             ERR_LDIFMODIFY_WRITE_ERROR.get(
                  targetLDIF.getValue().getAbsolutePath(),
                  StaticUtils.getExceptionMessage(e)));
        return ResultCode.LOCAL_ERROR;
      }
    }
    finally
    {
      if (ldifReader != null)
      {
        try
        {
          ldifReader.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
          logCompletionMessage(true,
               ERR_LDIFMODIFY_ERROR_CLOSING_READER.get(
                    sourceLDIF.getValue().getAbsolutePath(),
                    StaticUtils.getExceptionMessage(e)));
        }
      }

      if (ldifWriter != null)
      {
        try
        {
          ldifWriter.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
          logCompletionMessage(true,
               ERR_LDIFMODIFY_ERROR_CLOSING_WRITER.get(
                    sourceLDIF.getValue().getAbsolutePath(),
                    StaticUtils.getExceptionMessage(e)));
        }
      }
    }


    // If no entries were read and no updates were applied, then we'll consider
    // that an error, regardless of whether a read error was encountered.
    if ((entriesRead.get() == 0L) && (entriesUpdated.get() == 0L))
    {
      if (resultCode.get() == null)
      {
        logCompletionMessage(true,
             ERR_LDIFMODIFY_NO_SOURCE_ENTRIES.get(
                  sourceLDIF.getValue().getAbsolutePath()));
        return ResultCode.PARAM_ERROR;
      }
      else
      {
        logCompletionMessage(true,
             ERR_LDIFMODIFY_COULD_NOT_READ_SOURCE_ENTRIES.get(
                  sourceLDIF.getValue().getAbsolutePath()));
        return resultCode.get();
      }
    }


    // If no entries were updated, then we'll also consider that an error.
    if ((entriesUpdated.get() == 0L) && (! changesIgnored))
    {
      logCompletionMessage(true,
           ERR_LDIFMODIFY_NO_CHANGES_APPLIED_WITH_ERRORS.get(
                changesLDIF.getValue().getAbsolutePath(),
                sourceLDIF.getValue().getAbsolutePath()));
      resultCode.compareAndSet(null, ResultCode.PARAM_ERROR);
      return resultCode.get();
    }


    // Create the final completion message that will be used.
    final long entriesNotUpdated =
         Math.max((entriesRead.get() - entriesUpdated.get()), 0);
    if (resultCode.get() == null)
    {
      logCompletionMessage(false,
           INFO_LDIFMODIFY_COMPLETED_SUCCESSFULLY.get(entriesRead.get(),
                entriesUpdated.get(), entriesNotUpdated));
      return ResultCode.SUCCESS;
    }
    else
    {
      logCompletionMessage(true,
           ERR_LDIFMODIFY_COMPLETED_WITH_ERRORS.get(entriesRead.get(),
                entriesUpdated.get(), entriesNotUpdated));
      return resultCode.get();
    }
  }



  /**
   * Reads all of the LDIF change records from the changes file into a list.
   *
   * @param  addAndSubsequentChangeRecords
   *              A map that will be updated with add change records for a given
   *              entry, along with any subsequent change records that apply to
   *              the entry after it has been added.  It must not be
   *              {@code null}, must be empty, and must be updatable.
   * @param  deletedEntryDNs
   *              A map that will be updated with the DNs of any entries that
   *              are targeted by delete modifications and that have not been
   *              previously added or renamed.  It must not be {@code null},
   *              must be empty, and must be updatable.
   * @param  modifyChangeRecords
   *              A map that will be updated with any modify change records
   *              that target an entry that has not been targeted by any other
   *              type of change.  It must not be {@code null}, must be empty,
   *              and must be updatable.
   * @param  modifyDNAndSubsequentChangeRecords
   *              A map that will be updated with any change records for modify
   *              DN operations that target a given entry, and any subsequent
   *              operations that target the entry with its new DN.  It must not
   *              be {@code null}, must be empty, and must be updatable.
   * @param  resultCode
   *              A reference to the final result code that should be used for
   *              the tool.  This may be updated if an error occurred during
   *              processing and no value is already set.  It must not be
   *              {@code null}, but is allowed to have no value assigned.
   *
   * @throws  LDAPException  If an unrecoverable error occurs during processing.
   */
  private void readChangeRecords(
       @NotNull final Map>
            addAndSubsequentChangeRecords,
       @NotNull final Map deletedEntryDNs,
       @NotNull final Map> modifyChangeRecords,
       @NotNull final Map>>
            modifyDNAndSubsequentChangeRecords,
       @NotNull final AtomicReference resultCode)
       throws LDAPException
  {
    LDIFException firstRecoverableException = null;
    try (LDIFReader ldifReader = getLDIFReader(changesReader,
         changesLDIF.getValue(), changesEncryptionPassphraseFile.getValue()))
    {
changeRecordLoop:
      while (true)
      {
        // Read the next record from the changes file.
        final LDIFRecord ldifRecord;
        try
        {
          ldifRecord = ldifReader.readLDIFRecord();
        }
        catch (final LDIFException e)
        {
          Debug.debugException(e);

          if (e.mayContinueReading())
          {
            if (firstRecoverableException == null)
            {
              firstRecoverableException = e;
            }

            err();
            wrapErr(ERR_LDIFMODIFY_CANNOT_READ_RECORD_CAN_CONTINUE.get(
                 changesLDIF.getValue().getAbsolutePath(),
                 StaticUtils.getExceptionMessage(e)));
            resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
            continue changeRecordLoop;
          }
          else
          {
            throw new LDAPException(ResultCode.DECODING_ERROR,
                 ERR_LDIFMODIFY_CANNOT_READ_RECORD_CANNOT_CONTINUE.get(
                      changesLDIF.getValue().getAbsolutePath(),
                      StaticUtils.getExceptionMessage(e)),
                 e);
          }
        }

        if (ldifRecord == null)
        {
          break;
        }


        // Make sure that we can parse the DN for the change record.  If not,
        // then that's an error.
        final DN parsedDN;
        try
        {
          parsedDN = ldifRecord.getParsedDN();
        }
        catch (final LDAPException e)
        {
          Debug.debugException(e);

          err();
          wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_CHANGE_RECORD_DN.get(
               String.valueOf(ldifRecord),
               changesLDIF.getValue().getAbsolutePath(), e.getMessage()));
          resultCode.compareAndSet(null, e.getResultCode());
          continue changeRecordLoop;
        }


        // Get the LDIF record as a change record.  If the record is an entry
        // rather than a change record, then we'll treat it as an add change
        // record.
        final LDIFChangeRecord changeRecord;
        if (ldifRecord instanceof Entry)
        {
          changeRecord = new LDIFAddChangeRecord((Entry) ldifRecord);
        }
        else
        {
          changeRecord = (LDIFChangeRecord) ldifRecord;
        }


        // If the change record is for a modify DN, then make sure that we can
        // parse the new DN.
        final DN parsedNewDN;
        if (changeRecord.getChangeType() == ChangeType.MODIFY_DN)
        {
          try
          {
            parsedNewDN = ((LDIFModifyDNChangeRecord) changeRecord).getNewDN();
          }
          catch (final LDAPException e)
          {
            Debug.debugException(e);

            err();
            wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_NEW_DN.get(
                 String.valueOf(changeRecord),
                 changesLDIF.getValue().getAbsolutePath(), e.getMessage()));
            resultCode.compareAndSet(null, e.getResultCode());
            continue changeRecordLoop;
          }
        }
        else
        {
          parsedNewDN = parsedDN;
        }


        // Look at the change type and determine how to handle the operation.
        switch (changeRecord.getChangeType())
        {
          case ADD:
            // Make sure that we haven't already seen an add for an entry with
            // the same DN (unless that add was subsequently deleted).
            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_MULTIPLE_ADDS_FOR_DN.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
              continue changeRecordLoop;
            }

            // Make sure that there are no modifies targeting an entry with the
            // same DN.
            if (modifyChangeRecords.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_ADD_TARGETS_MODIFIED_ENTRY.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
              continue changeRecordLoop;
            }

            // Make sure that there aren't any modify DN operations that will
            // create an entry with the same or a subordinate DN.
            for (final Map.Entry>> e :
                 modifyDNAndSubsequentChangeRecords.entrySet())
            {
              final DN newDN = e.getValue().getFirst();
              if (parsedDN.isAncestorOf(newDN, true))
              {
                err();
                wrapErr(ERR_LDIFMODIFY_ADD_CONFLICTS_WITH_MOD_DN.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString(), e.getKey().toString(),
                     newDN.toString()));
                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
                continue changeRecordLoop;
              }
            }

            final List addList = new ArrayList<>();
            addList.add(changeRecord);
            addAndSubsequentChangeRecords.put(parsedDN, addList);
            break;


          case DELETE:
            // If the set of changes already included an add for this entry,
            // then remove that add and any subsequent changes for it.  This
            // isn't an error, so we don't need to set a result code.
            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              addAndSubsequentChangeRecords.remove(parsedDN);
              err();
              wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUS_ADD.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify DN that targeted
            // the entry, then reject the change.
            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              final DN newDN =
                   modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst();

              err();
              wrapErr(ERR_LDIFMODIFY_DELETE_OF_PREVIOUS_RENAME.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString(), newDN.toString()));
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify DN whose new DN
            // equals or is subordinate to the DN for the delete change
            // record, then remove that modify DN operation and any subsequent
            // changes for it, and instead add a delete for the original DN.
            // This isn't an error, so we don't need to set a result code.
            final Iterator>>>
                 deleteModDNIterator =
                 modifyDNAndSubsequentChangeRecords.entrySet().iterator();
            while (deleteModDNIterator.hasNext())
            {
              final Map.Entry>> e =
                   deleteModDNIterator.next();
              final DN newDN = e.getValue().getFirst();
              if (parsedDN.isAncestorOf(newDN, true))
              {
                final DN originalDN = e.getKey();
                deleteModDNIterator.remove();
                deletedEntryDNs.put(originalDN, Boolean.FALSE);

                err();
                wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_RENAMED.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString(), originalDN.toString(),
                     newDN.toString()));
                continue changeRecordLoop;
              }
            }

            // If the set of changes already included a delete for the same
            // DN, then reject the new change.
            if (deletedEntryDNs.containsKey(parsedDN))
            {
              if (! ignoreDuplicateDeletes.isPresent())
              {
                err();
                wrapErr(ERR_LDIFMODIFY_MULTIPLE_DELETES_FOR_DN.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString()));
                resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              }
              continue changeRecordLoop;
            }

            // If the set of changes included any modifications for the same DN,
            // then remove those modifications.  This isn't an error, so we
            // don't need to set a result code.
            if (modifyChangeRecords.containsKey(parsedDN))
            {
              modifyChangeRecords.remove(parsedDN);
              err();
              wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_MODIFIED.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
            }

            deletedEntryDNs.put(parsedDN, Boolean.FALSE);
            break;


          case MODIFY:
            // If the set of changes already included an add for an entry with
            // the same DN, then add the modify change record to the set of
            // changes following that add.
            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              addAndSubsequentChangeRecords.get(parsedDN).add(changeRecord);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify DN for an entry
            // with the same DN, then reject the new change.
            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              final DN newDN =
                   modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst();

              err();
              wrapErr(ERR_LDIFMODIFY_MODIFY_OF_RENAMED_ENTRY.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString(), newDN.toString()));
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify DN that would
            // result in an entry with the same DN as the modify, then add
            // the modify change record to the modify DN record's change list.
            for (final Map.Entry>> e :
                 modifyDNAndSubsequentChangeRecords.entrySet())
            {
              if (parsedDN.equals(e.getValue().getFirst()))
              {
                e.getValue().getSecond().add(changeRecord);
                continue changeRecordLoop;
              }
            }

            // If the set of changes already included a delete for an entry with
            // the same DN, then reject the new change.
            if (deletedEntryDNs.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_MODIFY_OF_DELETED_ENTRY.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify for an entry with
            // the same DN, then add the new change to that list.
            if (modifyChangeRecords.containsKey(parsedDN))
            {
              modifyChangeRecords.get(parsedDN).add(
                   (LDIFModifyChangeRecord) changeRecord);
              continue changeRecordLoop;
            }

            // Start a new change record list for the modify operation.
            final List modList = new ArrayList<>();
            modList.add((LDIFModifyChangeRecord) changeRecord);
            modifyChangeRecords.put(parsedDN, modList);
            break;


          case MODIFY_DN:
            // If the set of changes already included an add for an entry with
            // the same DN, then reject the modify DN.
            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_ADDED_ENTRY.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM);
              continue changeRecordLoop;
            }

            // If the set of changes already included an add for an entry with
            // an entry at or below the new DN, then reject the modify DN.
            for (final DN addedDN : addAndSubsequentChangeRecords.keySet())
            {
              if (addedDN.isDescendantOf(parsedNewDN, true))
              {
                err();
                wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_ADD.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString(), parsedNewDN.toString(),
                     addedDN.toString()));
                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
                continue changeRecordLoop;
              }
            }

            // If the set of changes already included a modify DN for an entry
            // with the same DN, then reject the modify DN.
            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_MULTIPLE_MOD_DN_WITH_DN.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify DN for an entry
            // that set a new DN that matches the DN of the new record, then
            // reject the modify DN.
            for (final Map.Entry>> e :
                 modifyDNAndSubsequentChangeRecords.entrySet())
            {
              final DN newDN = e.getValue().getFirst();
              if (newDN.isDescendantOf(parsedDN, true))
              {
                err();
                wrapErr(
                     ERR_LDIFMODIFY_UNWILLING_TO_MODIFY_DN_MULTIPLE_TIMES.get(
                          changesLDIF.getValue().getAbsolutePath(),
                          parsedDN.toString(), parsedNewDN.toString(),
                          e.getKey().toString()));
                resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM);
                continue changeRecordLoop;
              }
            }

            // If the set of changes already included a modify DN that set a
            // new DN that is at or below the new DN, then reject the modify DN.
            for (final Map.Entry>> e :
                 modifyDNAndSubsequentChangeRecords.entrySet())
            {
              final DN newDN = e.getValue().getFirst();
              if (newDN.isDescendantOf(parsedNewDN, true))
              {
                err();
                wrapErr(ERR_LDIFMODIFY_MOD_DN_CONFLICTS_WITH_MOD_DN.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString(), parsedNewDN.toString(),
                     e.getKey().toString(), newDN.toString()));
                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
                continue changeRecordLoop;
              }
            }

            // If the set of changes already included a delete for an entry with
            //t he same DN, then reject the modify DN.
            if (deletedEntryDNs.containsKey(parsedDN))
            {
              err();
              wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_DELETED_ENTRY.get(
                   changesLDIF.getValue().getAbsolutePath(),
                   parsedDN.toString()));
              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
              continue changeRecordLoop;
            }

            // If the set of changes already included a modify for an entry that
            // is at or below the new DN, then reject the modify DN.
            for (final DN dn : modifyChangeRecords.keySet())
            {
              if (dn.isDescendantOf(parsedNewDN, true))
              {
                err();
                wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_MOD.get(
                     changesLDIF.getValue().getAbsolutePath(),
                     parsedDN.toString(), parsedNewDN.toString(),
                     dn.toString()));
                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
                continue changeRecordLoop;
              }
            }

            final List modDNList = new ArrayList<>();
            modDNList.add(changeRecord);
            modifyDNAndSubsequentChangeRecords.put(parsedDN,
                 new ObjectPair>(parsedNewDN,
                      modDNList));
            break;
        }
      }
    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      throw new LDAPException(e.getResultCode(),
           ERR_LDIFMODIFY_ERROR_OPENING_CHANGES_FILE.get(
                changesLDIF.getValue().getAbsolutePath(), e.getMessage()),
           e);
    }
    catch (final IOException e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_LDIFMODIFY_ERROR_READING_CHANGES_FILE.get(
                changesLDIF.getValue().getAbsolutePath(),
                StaticUtils.getExceptionMessage(e)),
           e);
    }

    if (addAndSubsequentChangeRecords.isEmpty() && deletedEntryDNs.isEmpty() &&
         modifyChangeRecords.isEmpty() &&
         modifyDNAndSubsequentChangeRecords.isEmpty())
    {
      if (firstRecoverableException == null)
      {
        throw new LDAPException(ResultCode.PARAM_ERROR,
             ERR_LDIFMODIFY_NO_CHANGES.get(
                  changesLDIF.getValue().getAbsolutePath()));
      }
      else
      {
        throw new LDAPException(ResultCode.PARAM_ERROR,
             ERR_LDIFMODIFY_NO_CHANGES_WITH_ERROR.get(
                  changesLDIF.getValue().getAbsolutePath()),
             firstRecoverableException);
      }
    }
  }



  /**
   * Retrieves an LDIF reader that may be used to read LDIF records (either
   * entries or change records) from the specified LDIF file.
   *
   * @param  existingReader  An LDIF reader that was already provided to the
   *                         tool for this purpose.  It may be {@code null} if
   *                         the LDIF reader should be created with the given
   *                         LDIF file and passphrase file.
   * @param  ldifFile        The LDIF file for which to create the reader.  It
   *                         may be {@code null} only if {@code existingReader}
   *                         is non-{@code null}.
   * @param  passphraseFile  The file containing the encryption passphrase
   *                         needed to decrypt the contents of the provided LDIF
   *                         file.  It may be {@code null} if the LDIF file is
   *                         not encrypted or if the user should be
   *                         interactively prompted for the passphrase.
   *
   * @return  The LDIF reader that was created.
   *
   * @throws  LDAPException  If a problem occurs while creating the LDIF reader.
   */
  @NotNull()
  private LDIFReader getLDIFReader(@Nullable final LDIFReader existingReader,
                                   @Nullable final File ldifFile,
                                   @Nullable final File passphraseFile)
          throws LDAPException
  {
    if (existingReader != null)
    {
      return existingReader;
    }

    if (passphraseFile != null)
    {
      readPassphraseFile(passphraseFile);
    }


    boolean closeStream = true;
    InputStream inputStream = null;
    try
    {
      inputStream = new FileInputStream(ldifFile);

      final ObjectPair p =
           ToolUtils.getPossiblyPassphraseEncryptedInputStream(
                inputStream, inputEncryptionPassphrases,
                (passphraseFile != null),
                INFO_LDIFMODIFY_ENTER_INPUT_ENCRYPTION_PW.get(
                     ldifFile.getName()),
                ERR_LDIFMODIFY_WRONG_ENCRYPTION_PW.get(), getOut(), getErr());
      inputStream = p.getFirst();
      addPassphrase(p.getSecond());

      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);

      final LDIFReader ldifReader = new LDIFReader(inputStream);
      if (stripTrailingSpaces.isPresent())
      {
        ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
      }
      else
      {
        ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT);
      }

      ldifReader.setSchema(Schema.getDefaultStandardSchema());

      closeStream = false;
      return ldifReader;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_LDIFMODIFY_ERROR_OPENING_INPUT_FILE.get(
                ldifFile.getAbsolutePath(),
                StaticUtils.getExceptionMessage(e)),
           e);
    }
    finally
    {
      if ((inputStream != null) && closeStream)
      {
        try
        {
          inputStream.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }
    }
  }



  /**
   * Reads the contents of the specified passphrase file and adds it to the list
   * of passphrases.
   *
   * @param  f  The passphrase file to read.
   *
   * @throws  LDAPException  If a problem is encountered while trying to read
   *                         the passphrase from the provided file.
   */
  private void readPassphraseFile(@NotNull final File f)
          throws LDAPException
  {
    try
    {
      addPassphrase(getPasswordFileReader().readPassword(f));
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_LDIFMODIFY_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(),
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Updates the list of encryption passphrases with the provided passphrase, if
   * it is not already present.
   *
   * @param  passphrase  The passphrase to be added.  It may optionally be
   *                     {@code null} (in which case no action will be taken).
   */
  private void addPassphrase(@Nullable final char[] passphrase)
  {
    if (passphrase == null)
    {
      return;
    }

    for (final char[] existingPassphrase : inputEncryptionPassphrases)
    {
      if (Arrays.equals(existingPassphrase, passphrase))
      {
        return;
      }
    }

    inputEncryptionPassphrases.add(passphrase);
  }



  /**
   * Creates the LDIF writer to use to write the output.
   *
   * @param  existingWriter  An LDIF writer that was already provided to the
   *                         tool for this purpose.  It may be {@code null} if
   *                         the LDIF writer should be created using the
   *                         provided arguments.
   *
   * @return  The LDIF writer that was created.
   *
   * @throws  LDAPException  If a problem occurs while creating the LDIF writer.
   */
  @NotNull()
  private LDIFWriter getLDIFWriter(@Nullable final LDIFWriter existingWriter)
          throws LDAPException
  {
    if (existingWriter != null)
    {
      return existingWriter;
    }

    final File outputFile = targetLDIF.getValue();
    final File passphraseFile = targetEncryptionPassphraseFile.getValue();


    OutputStream outputStream = null;
    boolean closeOutputStream = true;
    try
    {
      try
      {

        outputStream = new FileOutputStream(targetLDIF.getValue());
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        throw new LDAPException(ResultCode.LOCAL_ERROR,
             ERR_LDIFMODIFY_CANNOT_OPEN_OUTPUT_FILE.get(
                  outputFile.getAbsolutePath(),
                  StaticUtils.getExceptionMessage(e)),
             e);
      }

      if (encryptTarget.isPresent())
      {
        try
        {
          final char[] passphrase;
          if (passphraseFile != null)
          {
            passphrase = getPasswordFileReader().readPassword(passphraseFile);
          }
          else
          {
            passphrase = ToolUtils.promptForEncryptionPassphrase(false, true,
                 INFO_LDIFMODIFY_ENTER_OUTPUT_ENCRYPTION_PW.get(),
                 INFO_LDIFMODIFY_CONFIRM_OUTPUT_ENCRYPTION_PW.get(), getOut(),
                 getErr()).toCharArray();
          }

          outputStream = new PassphraseEncryptedOutputStream(passphrase,
               outputStream, null, true, true);
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          throw new LDAPException(ResultCode.LOCAL_ERROR,
               ERR_LDIFMODIFY_CANNOT_ENCRYPT_OUTPUT_FILE.get(
                    outputFile.getAbsolutePath(),
                    StaticUtils.getExceptionMessage(e)),
               e);
        }
      }

      if (compressTarget.isPresent())
      {
        try
        {
          outputStream = new GZIPOutputStream(outputStream);
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          throw new LDAPException(ResultCode.LOCAL_ERROR,
               ERR_LDIFMODIFY_CANNOT_COMPRESS_OUTPUT_FILE.get(
                    outputFile.getAbsolutePath(),
                    StaticUtils.getExceptionMessage(e)),
               e);
        }
      }

      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
      if (doNotWrap.isPresent())
      {
        ldifWriter.setWrapColumn(0);
      }
      else if (wrapColumn.isPresent())
      {
        ldifWriter.setWrapColumn(wrapColumn.getValue());
      }
      else
      {
        ldifWriter.setWrapColumn(WRAP_COLUMN);
      }

      closeOutputStream = false;
      return ldifWriter;
    }
    finally
    {
      if (closeOutputStream && (outputStream != null))
      {
        try
        {
          outputStream.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }
    }
  }



  /**
   * Updates the provided entry with any appropriate changes.
   *
   * @param  entry
   *              The entry to be processed.  It must not be {@code null}.
   * @param  addAndSubsequentChangeRecords
   *              A map that will be updated with add change records for a given
   *              entry, along with any subsequent change records that apply to
   *              the entry after it has been added.  It must not be
   *              {@code null}, must be empty, and must be updatable.
   * @param  deletedEntryDNs
   *              A map that will be updated with the DNs of any entries that
   *              are targeted by delete modifications and that have not been
   *              previously added or renamed.  It must not be {@code null},
   *              must be empty, and must be updatable.
   * @param  modifyChangeRecords
   *              A map that will be updated with any modify change records
   *              that target an entry that has not been targeted by any other
   *              type of change.  It must not be {@code null}, must be empty,
   *              and must be updatable.
   * @param  modifyDNAndSubsequentChangeRecords
   *              A map that will be updated with any change records for modify
   *              DN operations that target a given entry, and any subsequent
   *              operations that target the entry with its new DN.  It must not
   *              be {@code null}, must be empty, and must be updatable.
   * @param  comment
   *              A buffer that should be updated with any comment to be
   *              included in the output, even if the entry is not altered.  It
   *              must not be {@code null}, but it should be empty.
   * @param  resultCode
   *              A reference to the final result code that should be used for
   *              the tool.  This may be updated if an error occurred during
   *              processing and no value is already set.  It must not be
   *              {@code null}, but is allowed to have no value assigned.
   * @param  entriesUpdated
   *              A counter that should be incremented if any changes are
   *              applied (including deleting the entry).  It should  not be
   *              updated if none of the changes are applicable to the provided
   *              entry.  It must not be {@code null}.
   *
   * @return  The provided entry if none of the changes are applicable, an
   *          updated entry if changes are applied, or {@code null} if the entry
   *          should be deleted and therefore omitted from the target LDIF file.
   */
  @Nullable()
  private Entry updateEntry(@NotNull final Entry entry,
       @NotNull final Map>
            addAndSubsequentChangeRecords,
       @NotNull final Map deletedEntryDNs,
       @NotNull final Map> modifyChangeRecords,
       @NotNull final Map>>
            modifyDNAndSubsequentChangeRecords,
       @NotNull final StringBuilder comment,
       @NotNull final AtomicReference resultCode,
       @NotNull final AtomicLong entriesUpdated)
  {
    // Get the parsed DN for the entry.  If that fails, then we'll just return
    // the provided entry along with a comment explaining that its DN could not
    // be parsed.
    final DN entryDN;
    try
    {
      entryDN = entry.getParsedDN();

    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      resultCode.compareAndSet(null, e.getResultCode());
      appendComment(comment,
           ERR_LDIFMODIFY_CANNOT_PARSE_ENTRY_DN.get(e.getMessage()), true);
      return entry;
    }


    // See if there is a delete change record for the entry.  If so, then mark
    // the entry as deleted and return null.
    if (deletedEntryDNs.containsKey(entryDN))
    {
      deletedEntryDNs.put(entryDN, Boolean.TRUE);
      createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_DELETE.get(),
           entry, false);
      entriesUpdated.incrementAndGet();
      return null;
    }


    // See if there is a delete change record for one of the entry's superiors.
    // If so, then mark the entry as deleted and return null.
    DN parentDN = entryDN.getParent();
    while (parentDN != null)
    {
      if (deletedEntryDNs.containsKey(parentDN))
      {
        createChangeRecordComment(comment,
             INFO_LDIFMODIFY_APPLIED_DELETE_OF_ANCESTOR.get(
                  parentDN.toString()),
             entry, false);
        entriesUpdated.incrementAndGet();
        return null;
      }

      parentDN = parentDN.getParent();
    }


    // See if there are any modify change records that target the entry.  If so,
    // then apply those modifications.
    Entry updatedEntry = entry;
    final AtomicBoolean isUpdated = new AtomicBoolean(false);
    final List errors = new ArrayList<>();
    final List modRecords =
         modifyChangeRecords.remove(entryDN);
    if (modRecords != null)
    {
      for (final LDIFModifyChangeRecord r : modRecords)
      {
        updatedEntry = applyModification(updatedEntry, r, isUpdated, resultCode,
             comment);
      }
    }


    // See if the entry was targeted by a modify DN operation.  If so, then
    // rename the entry and see if there are any follow-on modifications.
    final ObjectPair> modDNRecords =
         modifyDNAndSubsequentChangeRecords.remove(entryDN);
    if (modDNRecords != null)
    {
      for (final LDIFChangeRecord r : modDNRecords.getSecond())
      {
        if (r instanceof LDIFModifyDNChangeRecord)
        {
          final LDIFModifyDNChangeRecord modDNChangeRecord =
               (LDIFModifyDNChangeRecord) r;
          updatedEntry = applyModifyDN(updatedEntry, entryDN,
               modDNRecords.getFirst(), modDNChangeRecord.deleteOldRDN());
          createChangeRecordComment(comment,
               INFO_LDIFMODIFY_APPLIED_MODIFY_DN.get(), r, false);
          isUpdated.set(true);
        }
        else
        {
          updatedEntry = applyModification(updatedEntry,
               (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment);
        }
      }
    }


    // See if there is an add change record that targets the same entry.  If so,
    // then the add won't be processed but maybe subsequent changes will be.
    final List addAndMods =
         addAndSubsequentChangeRecords.remove(entryDN);
    if (addAndMods != null)
    {
      for (final LDIFChangeRecord r : addAndMods)
      {
        if (r instanceof LDIFAddChangeRecord)
        {
          resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
          createChangeRecordComment(comment,
               ERR_LDIFMODIFY_NOT_ADDING_EXISTING_ENTRY.get(), r, true);
        }
        else
        {
          updatedEntry = applyModification(updatedEntry,
               (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment);
        }
      }
    }


    if (isUpdated.get())
    {
      entriesUpdated.incrementAndGet();
    }
    else
    {
      if (comment.length() > 0)
      {
        appendComment(comment, StaticUtils.EOL, false);
        appendComment(comment, StaticUtils.EOL, false);
      }
      appendComment(comment, INFO_LDIFMODIFY_ENTRY_NOT_UPDATED.get(), false);
    }

    return updatedEntry;
  }



  /**
   * Creates a copy of the provided entry with the given modification applied.
   *
   * @param  entry               The entry to be updated.  It must not be
   *                             {@code null}.
   * @param  modifyChangeRecord  The modify change record to apply.  It must not
   *                             be {@code null}.
   * @param  isUpdated           A value that should be updated if the entry is
   *                             successfully modified.  It must not be
   *                             {@code null}.
   * @param  resultCode          A reference to the final result code that
   *                             should be used for the tool.  This may be
   *                             updated if an error occurred during processing
   *                             and no value is already set.  It must not be
   *                             {@code null}, but is allowed to have no value
   *                             assigned.
   * @param  comment             A buffer that should be updated with any
   *                             comment to be included in the output, even if
   *                             the entry is not altered.  It must not be
   *                             {@code null}, but it may be empty.
   *
   * @return  The entry with the modifications applied, or the original entry if
   *          an error occurred while applying the change.
   */
  @NotNull()
  private Entry applyModification(@NotNull final Entry entry,
                     @NotNull final LDIFModifyChangeRecord modifyChangeRecord,
                     @NotNull final AtomicBoolean isUpdated,
                     @NotNull final AtomicReference resultCode,
                     @NotNull final StringBuilder comment)
  {
    try
    {
      final Entry updatedEntry = Entry.applyModifications(entry,
           (! strictModifications.isPresent()),
           modifyChangeRecord.getModifications());
      createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_MODIFY.get(),
           modifyChangeRecord, false);
      isUpdated.set(true);
      return updatedEntry;
    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      resultCode.compareAndSet(null, e.getResultCode());
      createChangeRecordComment(comment,
           ERR_LDIFMODIFY_ERROR_APPLYING_MODIFY.get(
                String.valueOf(e.getResultCode()), e.getMessage()),
           modifyChangeRecord, true);
      return entry;
    }
  }



  /**
   * Creates a copy of the provided entry with the given new DN.
   *
   * @param  entry         The entry to be renamed.  It must not be
   *                       {@code null}.
   * @param  originalDN    A parsed representation of the original DN for the
   *                       entry.  It must not be {@code null}.
   * @param  newDN         A parsed representation of the new DN for the entry.
   *                       It must not be {@code null}.
   * @param  deleteOldRDN  Indicates whether the old RDN values should be
   *                       removed from the entry.
   *
   * @return  The updated entry with the new DN and any other associated
   *          changes.
   */
  @NotNull()
  private Entry applyModifyDN(@NotNull final Entry entry,
                              @NotNull final DN originalDN,
                              @NotNull final DN newDN,
                              final boolean deleteOldRDN)
  {
    final Entry copy = entry.duplicate();
    copy.setDN(newDN);

    final RDN oldRDN = originalDN.getRDN();
    if (deleteOldRDN && (oldRDN != null))
    {
      for (final Attribute a : oldRDN.getAttributes())
      {
        for (final byte[] value : a.getValueByteArrays())
        {
          copy.removeAttributeValue(a.getName(), value);
        }
      }
    }

    final RDN newRDN = newDN.getRDN();
    if (newRDN != null)
    {
      for (final Attribute a : newRDN.getAttributes())
      {
        for (final byte[] value : a.getValueByteArrays())
        {
          copy.addAttribute(a);
        }
      }
    }

    return copy;
  }



  /**
   * Writes the provided LDIF record to the LDIF writer.
   *
   * @param  ldifWriter  The writer to which the LDIF record should be written.
   *                     It must not be {@code null}.
   * @param  ldifRecord  The LDIF record to be written.  It must not be
   *                     {@code null}.
   * @param  comment     The comment to include as part of the LDIF record.  It
   *                     may be {@code null} or empty if no comment should be
   *                     included.
   *
   * @throws  IOException  If an error occurs while attempting to write to the
   *                       LDIF writer.
   */
  private void writeLDIFRecord(@NotNull final LDIFWriter ldifWriter,
                               @NotNull final LDIFRecord ldifRecord,
                               @Nullable final CharSequence comment)
          throws IOException
  {
    if (suppressComments.isPresent() || (comment == null) ||
         (comment.length() == 0))
    {
      ldifWriter.writeLDIFRecord(ldifRecord);
    }
    else
    {
      ldifWriter.writeLDIFRecord(ldifRecord, comment.toString());
    }
  }



  /**
   * Appends the provided comment to the given buffer.
   *
   * @param  buffer   The buffer to which the comment should be appended.
   * @param  comment  The comment to be appended.
   * @param  isError  Indicates whether the comment represents an error that
   *                  should be added to the error list if it exists.  It should
   *                  be {@code false} if the comment is not an error, or if it
   *                  is an error but should not be added to the list of error
   *                  messages (e.g., because a message will be added through
   *                  some other means).
   */
  private void appendComment(@NotNull final StringBuilder buffer,
                             @NotNull final String comment,
                             final boolean isError)
  {
    buffer.append(comment);
    if (isError && (errorMessages != null))
    {
      errorMessages.add(comment);
    }
  }



  /**
   * Writes the provided comment to the LDIF writer.
   *
   * @param  ldifWriter  The writer to which the comment should be written.  It
   *                     must not be {@code null}.
   * @param  comment     The comment to be written.  It may be {@code null} or
   *                     empty if no comment should actually be written.
   * @param  isError     Indicates whether the comment represents an error that
   *                     should be added to the error list if it exists.  It
   *                     should be {@code false} if the comment is not an error,
   *                     or if it is an error but should not be added to the
   *                     list of error messages (e.g., because a message will be
   *                     added through some other means).
   *
   * @throws  IOException  If an error occurs while attempting to write to the
   *                       LDIF writer.
   */
  private void writeLDIFComment(@NotNull final LDIFWriter ldifWriter,
                                @Nullable final CharSequence comment,
                                final boolean isError)
          throws IOException
  {
    if (! (suppressComments.isPresent() || (comment == null) ||
         (comment.length() == 0)))
    {
      ldifWriter.writeComment(comment.toString(), false, true);
    }

    if (isError && (errorMessages != null) && (comment != null))
    {
      errorMessages.add(comment.toString());
    }
  }



  /**
   * Appends a comment to the provided buffer for the given LDIF record.
   *
   * @param  buffer   The buffer to which the comment should be appended.  It
   *                  must not be {@code null}.
   * @param  message  The message to include before the LDIF record.  It must
   *                  not be {@code null}.
   * @param  record   The LDIF record to include in the comment.
   * @param  isError  Indicates whether the comment represents an error that
   *                  should be added to the error list if it exists.  It should
   *                  be {@code false} if the comment is not an error, or if it
   *                  is an error but should not be added to the list of error
   *                  messages (e.g., because a message will be added through
   *                  some other means).
   */
  private void createChangeRecordComment(@NotNull final StringBuilder buffer,
                                         @NotNull final String message,
                                         @NotNull final LDIFRecord record,
                                         final boolean isError)
  {
    final int initialLength = buffer.length();
    if (initialLength > 0)
    {
      buffer.append(StaticUtils.EOL);
      buffer.append(StaticUtils.EOL);
    }

    buffer.append(message);
    buffer.append(StaticUtils.EOL);

    final int wrapCol;
    if (wrapColumn.isPresent() && (wrapColumn.getValue() > 20) &&
         (wrapColumn.getValue() <= 85))
    {
      wrapCol = wrapColumn.getValue() - 10;
    }
    else
    {
      wrapCol = 75;
    }

    for (final String line : record.toLDIF(wrapCol))
    {
      buffer.append("     ");
      buffer.append(line);
      buffer.append(StaticUtils.EOL);
    }

    if (isError && (errorMessages != null))
    {
      if (initialLength == 0)
      {
        errorMessages.add(buffer.toString());
      }
      else
      {
        errorMessages.add(buffer.toString().substring(initialLength));
      }
    }
  }



  /**
   * Writes a wrapped version of the provided message to standard error.  If an
   * {@code errorList} is also available, then the message will also be added to
   * that list.
   *
   * @param  message  The message to be written.  It must not be {@code null].
   */
  private void wrapErr(@NotNull final String message)
  {
    wrapErr(0, WRAP_COLUMN, message);
    if (errorMessages != null)
    {
      errorMessages.add(message);
    }
  }



  /**
   * Writes the provided message and sets it as the completion message.
   *
   * @param  isError  Indicates whether the message should be written to
   *                  standard error rather than standard output.
   * @param  message  The message to be written.
   */
  private void logCompletionMessage(final boolean isError,
                                    @NotNull final String message)
  {
    completionMessage.compareAndSet(null, message);

    if (isError)
    {
      wrapErr(message);
    }
    else
    {
      wrapOut(0, WRAP_COLUMN, message);
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public LinkedHashMap getExampleUsages()
  {
    final LinkedHashMap examples = new LinkedHashMap<>();

    examples.put(
         new String[]
         {
           "--sourceLDIF", "original.ldif",
           "--changesLDIF", "changes.ldif",
           "--targetLDIF", "updated.ldif"
         },
         INFO_LDIFMODIFY_EXAMPLE.get("changes.ldif", "original.ldif",
              "updated.ldif"));

    return examples;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy