001/*
002 * Copyright 2020-2023 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2023 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-2023 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.InputStream;
044import java.io.OutputStream;
045import java.util.ArrayList;
046import java.util.Arrays;
047import java.util.Collections;
048import java.util.EnumSet;
049import java.util.HashSet;
050import java.util.LinkedHashMap;
051import java.util.LinkedHashSet;
052import java.util.List;
053import java.util.Map;
054import java.util.Set;
055import java.util.TreeMap;
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.Filter;
064import com.unboundid.ldap.sdk.InternalSDKHelper;
065import com.unboundid.ldap.sdk.LDAPException;
066import com.unboundid.ldap.sdk.Modification;
067import com.unboundid.ldap.sdk.ResultCode;
068import com.unboundid.ldap.sdk.Version;
069import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
070import com.unboundid.ldap.sdk.schema.Schema;
071import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
072import com.unboundid.util.CommandLineTool;
073import com.unboundid.util.Debug;
074import com.unboundid.util.NotNull;
075import com.unboundid.util.Nullable;
076import com.unboundid.util.ObjectPair;
077import com.unboundid.util.PassphraseEncryptedOutputStream;
078import com.unboundid.util.StaticUtils;
079import com.unboundid.util.ThreadSafety;
080import com.unboundid.util.ThreadSafetyLevel;
081import com.unboundid.util.args.ArgumentException;
082import com.unboundid.util.args.ArgumentParser;
083import com.unboundid.util.args.BooleanArgument;
084import com.unboundid.util.args.FileArgument;
085import com.unboundid.util.args.FilterArgument;
086import com.unboundid.util.args.StringArgument;
087
088import static com.unboundid.ldif.LDIFMessages.*;
089
090
091
092/**
093 * This class provides a command-line tool that can be used to identify the
094 * differences between two LDIF files.  The output will itself be an LDIF file
095 * that contains the add, delete, and modify operations that can be processed
096 * against the source LDIF file to result in the target LDIF file.
097 */
098@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
099public final class LDIFDiff
100       extends CommandLineTool
101{
102  /**
103   * The server root directory for the Ping Identity Directory Server (or
104   * related Ping Identity server product) that contains this tool, if
105   * applicable.
106   */
107  @Nullable private static final File PING_SERVER_ROOT =
108       InternalSDKHelper.getPingIdentityServerRoot();
109
110
111
112  /**
113   * Indicates whether the tool is running as part of a Ping Identity Directory
114   * Server (or related Ping Identity Server Product) installation.
115   */
116  private static final boolean PING_SERVER_AVAILABLE =
117       (PING_SERVER_ROOT != null);
118
119
120
121  /**
122   * The column at which to wrap long lines.
123   */
124  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
125
126
127
128  /**
129   * The change type name used to indicate that add operations should be
130   * included in the output.
131   */
132  @NotNull
133  private static final String CHANGE_TYPE_ADD = "add";
134
135
136
137  /**
138   * The change type name used to indicate that delete operations should be
139   * included in the output.
140   */
141  @NotNull private static final String CHANGE_TYPE_DELETE = "delete";
142
143
144
145  /**
146   * The change type name used to indicate that modify operations should be
147   * included in the output.
148   */
149  @NotNull private static final String CHANGE_TYPE_MODIFY = "modify";
150
151
152
153  // The completion message for this tool.
154  @NotNull private final AtomicReference<String> completionMessage;
155
156  // Encryption passphrases used thus far.
157  @NotNull private final List<char[]> encryptionPassphrases;
158
159  // The command-line arguments supported by this tool.
160  @Nullable private BooleanArgument compressOutput;
161  @Nullable private BooleanArgument encryptOutput;
162  @Nullable private BooleanArgument excludeNoUserModificationAttributes;
163  @Nullable private BooleanArgument includeOperationalAttributes;
164  @Nullable private BooleanArgument nonReversibleModifications;
165  @Nullable private BooleanArgument overwriteExistingOutputLDIF;
166  @Nullable private BooleanArgument singleValueChanges;
167  @Nullable private BooleanArgument stripTrailingSpaces;
168  @Nullable private FileArgument outputEncryptionPassphraseFile;
169  @Nullable private FileArgument outputLDIF;
170  @Nullable private FileArgument schemaPath;
171  @Nullable private FileArgument sourceEncryptionPassphraseFile;
172  @Nullable private FileArgument sourceLDIF;
173  @Nullable private FileArgument targetEncryptionPassphraseFile;
174  @Nullable private FileArgument targetLDIF;
175  @Nullable private FilterArgument excludeFilter;
176  @Nullable private FilterArgument includeFilter;
177  @Nullable private StringArgument changeType;
178  @Nullable private StringArgument excludeAttribute;
179  @Nullable private StringArgument includeAttribute;
180
181
182
183  /**
184   * Invokes this tool with the provided set of command-line arguments.
185   *
186   * @param  args  The set of arguments provided to this tool.  It may be
187   *               empty but must not be {@code null}.
188   */
189  public static void main(@NotNull final String... args)
190  {
191    final ResultCode resultCode = main(System.out, System.err, args);
192    if (resultCode != ResultCode.SUCCESS)
193    {
194      System.exit(resultCode.intValue());
195    }
196  }
197
198
199
200  /**
201   * Invokes this tool with the provided set of command-line arguments, using
202   * the given output and error streams.
203   *
204   * @param  out   The output stream to use for standard output.  It may be
205   *               {@code null} if standard output should be suppressed.
206   * @param  err   The output stream to use for standard error.  It may be
207   *               {@code null} if standard error should be suppressed.
208   * @param  args  The set of arguments provided to this tool.  It may be
209   *               empty but must not be {@code null}.
210   *
211   * @return  A result code indicating the status of processing.  Any result
212   *          code other than {@link ResultCode#SUCCESS} should be considered
213   *          an error.
214   */
215  @NotNull()
216  public static ResultCode main(@Nullable final OutputStream out,
217                                @Nullable final OutputStream err,
218                                @NotNull final String... args)
219  {
220    final LDIFDiff tool = new LDIFDiff(out, err);
221    return tool.runTool(args);
222  }
223
224
225
226  /**
227   * Creates a new instance of this tool with the provided output and error
228   * streams.
229   *
230   * @param  out  The output stream to use for standard output.  It may be
231   *              {@code null} if standard output should be suppressed.
232   * @param  err  The output stream to use for standard error.  It may be
233   *              {@code null} if standard error should be suppressed.
234   */
235  public LDIFDiff(@Nullable final OutputStream out,
236                  @Nullable final OutputStream err)
237  {
238    super(out, err);
239
240    encryptionPassphrases = new ArrayList<>(5);
241    completionMessage = new AtomicReference<>();
242
243    compressOutput = null;
244    encryptOutput = null;
245    excludeNoUserModificationAttributes = null;
246    includeOperationalAttributes = null;
247    nonReversibleModifications = null;
248    overwriteExistingOutputLDIF = null;
249    singleValueChanges = null;
250    stripTrailingSpaces = null;
251    outputEncryptionPassphraseFile = null;
252    outputLDIF = null;
253    schemaPath = null;
254    sourceEncryptionPassphraseFile = null;
255    sourceLDIF = null;
256    targetEncryptionPassphraseFile = null;
257    targetLDIF = null;
258    changeType = null;
259    excludeFilter = null;
260    includeFilter = null;
261    excludeAttribute = null;
262    includeAttribute = null;
263  }
264
265
266
267  /**
268   * {@inheritDoc}
269   */
270  @Override()
271  @NotNull()
272  public String getToolName()
273  {
274    return "ldif-diff";
275  }
276
277
278
279  /**
280   * {@inheritDoc}
281   */
282  @Override()
283  @NotNull()
284  public String getToolDescription()
285  {
286    return INFO_LDIF_DIFF_TOOL_DESCRIPTION_1.get();
287  }
288
289
290
291  /**
292   * {@inheritDoc}
293   */
294  @Override()
295  @NotNull()
296  public List<String> getAdditionalDescriptionParagraphs()
297  {
298    final List<String> messages = new ArrayList<>(3);
299    messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_2.get());
300    messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_3.get());
301
302    if (PING_SERVER_AVAILABLE)
303    {
304      messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_4_PING_SERVER.get(
305           getToolName()));
306    }
307    else
308    {
309      messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_4_STANDALONE.get(
310           getToolName()));
311    }
312
313    return messages;
314  }
315
316
317
318  /**
319   * {@inheritDoc}
320   */
321  @Override()
322  @NotNull()
323  public String getToolVersion()
324  {
325    return Version.NUMERIC_VERSION_STRING;
326  }
327
328
329
330  /**
331   * {@inheritDoc}
332   */
333  @Override()
334  public boolean supportsInteractiveMode()
335  {
336    return true;
337  }
338
339
340
341  /**
342   * {@inheritDoc}
343   */
344  @Override()
345  public boolean defaultsToInteractiveMode()
346  {
347    return true;
348  }
349
350
351
352  /**
353   * {@inheritDoc}
354   */
355  @Override()
356  public boolean supportsPropertiesFile()
357  {
358    return true;
359  }
360
361
362
363  /**
364   * {@inheritDoc}
365   */
366  @Override()
367  @Nullable()
368  protected String getToolCompletionMessage()
369  {
370    return completionMessage.get();
371  }
372
373
374
375  /**
376   * {@inheritDoc}
377   */
378  @Override()
379  public void addToolArguments(@NotNull final ArgumentParser parser)
380         throws ArgumentException
381  {
382    sourceLDIF = new FileArgument('s', "sourceLDIF", true, 1, null,
383         INFO_LDIF_DIFF_ARG_DESC_SOURCE_LDIF.get(), true, true, true, false);
384    sourceLDIF.addLongIdentifier("source-ldif", true);
385    sourceLDIF.addLongIdentifier("source", true);
386    sourceLDIF.addLongIdentifier("sourceFile", true);
387    sourceLDIF.addLongIdentifier("source-file", true);
388    sourceLDIF.addLongIdentifier("sourceLDIFFile", true);
389    sourceLDIF.addLongIdentifier("source-ldif-file", true);
390    sourceLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_SOURCE.get());
391    parser.addArgument(sourceLDIF);
392
393
394    final String sourcePWDesc;
395    if (PING_SERVER_AVAILABLE)
396    {
397      sourcePWDesc = INFO_LDIF_DIFF_ARG_DESC_SOURCE_PW_FILE_PING_SERVER.get();
398    }
399    else
400    {
401      sourcePWDesc = INFO_LDIF_DIFF_ARG_DESC_SOURCE_PW_FILE_STANDALONE.get();
402    }
403    sourceEncryptionPassphraseFile = new FileArgument(null,
404         "sourceEncryptionPassphraseFile", false, 1, null, sourcePWDesc, true,
405         true, true, false);
406    sourceEncryptionPassphraseFile.addLongIdentifier(
407         "source-encryption-passphrase-file", true);
408    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePassphraseFile",
409         true);
410    sourceEncryptionPassphraseFile.addLongIdentifier("source-passphrase-file",
411         true);
412    sourceEncryptionPassphraseFile.addLongIdentifier(
413         "sourceEncryptionPasswordFile", true);
414    sourceEncryptionPassphraseFile.addLongIdentifier(
415         "source-encryption-password-file", true);
416    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePasswordFile",
417         true);
418    sourceEncryptionPassphraseFile.addLongIdentifier("source-password-file",
419         true);
420    sourceEncryptionPassphraseFile.setArgumentGroupName(
421         INFO_LDIF_DIFF_ARG_GROUP_SOURCE.get());
422    parser.addArgument(sourceEncryptionPassphraseFile);
423
424
425    targetLDIF = new FileArgument('t', "targetLDIF", true, 1, null,
426         INFO_LDIF_DIFF_ARG_DESC_TARGET_LDIF.get(), true, true, true, false);
427    targetLDIF.addLongIdentifier("target-ldif", true);
428    targetLDIF.addLongIdentifier("target", true);
429    targetLDIF.addLongIdentifier("targetFile", true);
430    targetLDIF.addLongIdentifier("target-file", true);
431    targetLDIF.addLongIdentifier("targetLDIFFile", true);
432    targetLDIF.addLongIdentifier("target-ldif-file", true);
433    targetLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_TARGET.get());
434    parser.addArgument(targetLDIF);
435
436
437    final String targetPWDesc;
438    if (PING_SERVER_AVAILABLE)
439    {
440      targetPWDesc = INFO_LDIF_DIFF_ARG_DESC_TARGET_PW_FILE_PING_SERVER.get();
441    }
442    else
443    {
444      targetPWDesc = INFO_LDIF_DIFF_ARG_DESC_TARGET_PW_FILE_STANDALONE.get();
445    }
446    targetEncryptionPassphraseFile = new FileArgument(null,
447         "targetEncryptionPassphraseFile", false, 1, null, targetPWDesc, true,
448         true, true, false);
449    targetEncryptionPassphraseFile.addLongIdentifier(
450         "target-encryption-passphrase-file", true);
451    targetEncryptionPassphraseFile.addLongIdentifier("targetPassphraseFile",
452         true);
453    targetEncryptionPassphraseFile.addLongIdentifier("target-passphrase-file",
454         true);
455    targetEncryptionPassphraseFile.addLongIdentifier(
456         "targetEncryptionPasswordFile", true);
457    targetEncryptionPassphraseFile.addLongIdentifier(
458         "target-encryption-password-file", true);
459    targetEncryptionPassphraseFile.addLongIdentifier("targetPasswordFile",
460         true);
461    targetEncryptionPassphraseFile.addLongIdentifier("target-password-file",
462         true);
463    targetEncryptionPassphraseFile.setArgumentGroupName(
464         INFO_LDIF_DIFF_ARG_GROUP_TARGET.get());
465    parser.addArgument(targetEncryptionPassphraseFile);
466
467
468    outputLDIF = new FileArgument('o', "outputLDIF", false, 1, null,
469         INFO_LDIF_DIFF_ARG_DESC_OUTPUT_LDIF.get(), false, true, true, false);
470    outputLDIF.addLongIdentifier("output-ldif", true);
471    outputLDIF.addLongIdentifier("output", true);
472    outputLDIF.addLongIdentifier("outputFile", true);
473    outputLDIF.addLongIdentifier("output-file", true);
474    outputLDIF.addLongIdentifier("outputLDIFFile", true);
475    outputLDIF.addLongIdentifier("output-ldif-file", true);
476    outputLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get());
477    parser.addArgument(outputLDIF);
478
479
480    compressOutput = new BooleanArgument(null, "compressOutput", 1,
481         INFO_LDIF_DIFF_ARG_DESC_COMPRESS_OUTPUT.get());
482    compressOutput.addLongIdentifier("compress-output", true);
483    compressOutput.addLongIdentifier("compress", true);
484    compressOutput.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get());
485    parser.addArgument(compressOutput);
486
487
488    encryptOutput = new BooleanArgument(null, "encryptOutput", 1,
489         INFO_LDIF_DIFF_ARG_DESC_ENCRYPT_OUTPUT.get());
490    encryptOutput.addLongIdentifier("encrypt-output", true);
491    encryptOutput.addLongIdentifier("encrypt", true);
492    encryptOutput.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get());
493    parser.addArgument(encryptOutput);
494
495
496    outputEncryptionPassphraseFile = new FileArgument(null,
497         "outputEncryptionPassphraseFile", false, 1, null,
498         INFO_LDIF_DIFF_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true, false);
499    outputEncryptionPassphraseFile.addLongIdentifier(
500         "output-encryption-passphrase-file", true);
501    outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile",
502         true);
503    outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file",
504         true);
505    outputEncryptionPassphraseFile.addLongIdentifier(
506         "outputEncryptionPasswordFile", true);
507    outputEncryptionPassphraseFile.addLongIdentifier(
508         "output-encryption-password-file", true);
509    outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
510         true);
511    outputEncryptionPassphraseFile.addLongIdentifier("output-password-file",
512         true);
513    outputEncryptionPassphraseFile.setArgumentGroupName(
514         INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get());
515    parser.addArgument(outputEncryptionPassphraseFile);
516
517
518    overwriteExistingOutputLDIF = new BooleanArgument('O',
519         "overwriteExistingOutputLDIF", 1,
520         INFO_LDIF_DIFF_ARG_DESC_OVERWRITE_EXISTING.get());
521    overwriteExistingOutputLDIF.addLongIdentifier(
522         "overwrite-existing-output-ldif", true);
523    overwriteExistingOutputLDIF.addLongIdentifier(
524         "overwriteExistingOutputFile", true);
525    overwriteExistingOutputLDIF.addLongIdentifier(
526         "overwrite-existing-output-file", true);
527    overwriteExistingOutputLDIF.addLongIdentifier("overwriteExistingOutput",
528         true);
529    overwriteExistingOutputLDIF.addLongIdentifier("overwrite-existing-output",
530         true);
531    overwriteExistingOutputLDIF.addLongIdentifier("overwriteExisting", true);
532    overwriteExistingOutputLDIF.addLongIdentifier("overwrite-existing", true);
533    overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutputLDIF", true);
534    overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output-ldif",
535         true);
536    overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutputFile", true);
537    overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output-file",
538         true);
539    overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutput", true);
540    overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output", true);
541    overwriteExistingOutputLDIF.addLongIdentifier("overwrite", true);
542    overwriteExistingOutputLDIF.setArgumentGroupName(
543         INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get());
544    parser.addArgument(overwriteExistingOutputLDIF);
545
546
547    changeType = new StringArgument(null, "changeType", false, 0,
548         INFO_LDIF_DIFF_ARG_PLACEHOLDER_CHANGE_TYPE.get(),
549         INFO_LDIF_DIFF_ARG_DESC_CHANGE_TYPE.get(),
550         StaticUtils.setOf(
551              CHANGE_TYPE_ADD,
552              CHANGE_TYPE_DELETE,
553              CHANGE_TYPE_MODIFY),
554         Collections.unmodifiableList(Arrays.asList(
555              CHANGE_TYPE_ADD,
556              CHANGE_TYPE_DELETE,
557              CHANGE_TYPE_MODIFY)));
558    changeType.addLongIdentifier("change-type", true);
559    changeType.addLongIdentifier("operationType", true);
560    changeType.addLongIdentifier("operation-type", true);
561    changeType.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
562    parser.addArgument(changeType);
563
564
565    includeFilter = new FilterArgument(null, "includeFilter", false, 0, null,
566         INFO_LDIF_DIFF_ARG_DESC_INCLUDE_FILTER.get());
567    includeFilter.addLongIdentifier("include-filter", true);
568    includeFilter.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
569    parser.addArgument(includeFilter);
570
571
572    excludeFilter = new FilterArgument(null, "excludeFilter", false, 0, null,
573         INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_FILTER.get());
574    excludeFilter.addLongIdentifier("exclude-filter", true);
575    excludeFilter.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
576    parser.addArgument(excludeFilter);
577
578
579    includeAttribute = new StringArgument(null, "includeAttribute", false, 0,
580         INFO_LDIF_DIFF_ARG_PLACEHOLDER_ATTRIBUTE.get(),
581         INFO_LDIF_DIFF_ARG_DESC_INCLUDE_ATTRIBUTE.get());
582    includeAttribute.addLongIdentifier("include-attribute", true);
583    includeAttribute.addLongIdentifier("includeAttr", true);
584    includeAttribute.addLongIdentifier("include-attr", true);
585    includeAttribute.setArgumentGroupName(
586         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
587    parser.addArgument(includeAttribute);
588
589
590    excludeAttribute = new StringArgument(null, "excludeAttribute", false, 0,
591         INFO_LDIF_DIFF_ARG_PLACEHOLDER_ATTRIBUTE.get(),
592         INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_ATTRIBUTE.get());
593    excludeAttribute.addLongIdentifier("exclude-attribute", true);
594    excludeAttribute.addLongIdentifier("excludeAttr", true);
595    excludeAttribute.addLongIdentifier("exclude-attr", true);
596    excludeAttribute.setArgumentGroupName(
597         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
598    parser.addArgument(excludeAttribute);
599
600
601    includeOperationalAttributes = new BooleanArgument('i',
602         "includeOperationalAttributes", 1,
603         INFO_LDIF_DIFF_ARG_DESC_INCLUDE_OPERATIONAL.get());
604    includeOperationalAttributes.addLongIdentifier(
605         "include-operational-attributes", true);
606    includeOperationalAttributes.addLongIdentifier("includeOperational", true);
607    includeOperationalAttributes.addLongIdentifier("include-operational", true);
608    includeOperationalAttributes.setArgumentGroupName(
609         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
610    parser.addArgument(includeOperationalAttributes);
611
612
613    excludeNoUserModificationAttributes = new BooleanArgument('e',
614         "excludeNoUserModificationAttributes", 1,
615         INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_NO_USER_MOD.get());
616    excludeNoUserModificationAttributes.addLongIdentifier(
617         "exclude-no-user-modification-attributes", true);
618    excludeNoUserModificationAttributes.addLongIdentifier(
619         "excludeNoUserModAttributes", true);
620    excludeNoUserModificationAttributes.addLongIdentifier(
621         "exclude-no-user-mod-attributes", true);
622    excludeNoUserModificationAttributes.addLongIdentifier(
623         "excludeNoUserModification", true);
624    excludeNoUserModificationAttributes.addLongIdentifier(
625         "exclude-no-user-modification", true);
626    excludeNoUserModificationAttributes.addLongIdentifier("excludeNoUserMod",
627         true);
628    excludeNoUserModificationAttributes.addLongIdentifier("exclude-no-user-mod",
629         true);
630    excludeNoUserModificationAttributes.setArgumentGroupName(
631         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
632    parser.addArgument(excludeNoUserModificationAttributes);
633
634
635    nonReversibleModifications = new BooleanArgument(null,
636         "nonReversibleModifications", 1,
637         INFO_LDIF_DIFF_ARG_DESC_NON_REVERSIBLE_MODS.get());
638    nonReversibleModifications.addLongIdentifier("non-reversible-modifications",
639         true);
640    nonReversibleModifications.addLongIdentifier("nonReversibleMods", true);
641    nonReversibleModifications.addLongIdentifier("non-reversible-mods", true);
642    nonReversibleModifications.addLongIdentifier("nonReversible", true);
643    nonReversibleModifications.addLongIdentifier("non-reversible", true);
644    nonReversibleModifications.setArgumentGroupName(
645         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
646    parser.addArgument(nonReversibleModifications);
647
648
649    singleValueChanges = new BooleanArgument('S', "singleValueChanges", 1,
650         INFO_LDIF_DIFF_ARG_DESC_SINGLE_VALUE_CHANGES.get());
651    singleValueChanges.addLongIdentifier("single-value-changes", true);
652    parser.addArgument(singleValueChanges);
653
654
655    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
656         INFO_LDIF_DIFF_ARG_DESC_STRIP_TRAILING_SPACES.get());
657    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
658    stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true);
659    stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true);
660    stripTrailingSpaces.setArgumentGroupName(
661         INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get());
662    parser.addArgument(stripTrailingSpaces);
663
664
665    final String schemaPathDesc;
666    if (PING_SERVER_AVAILABLE)
667    {
668      schemaPathDesc = INFO_LDIF_DIFF_ARG_DESC_SCHEMA_PATH_PING_SERVER.get();
669    }
670    else
671    {
672      schemaPathDesc = INFO_LDIF_DIFF_ARG_DESC_SCHEMA_PATH_STANDALONE.get();
673    }
674    schemaPath = new FileArgument(null, "schemaPath", false, 0, null,
675         schemaPathDesc, true, true, false, false);
676    schemaPath.addLongIdentifier("schema-path", true);
677    schemaPath.addLongIdentifier("schemaFile", true);
678    schemaPath.addLongIdentifier("schema-file", true);
679    schemaPath.addLongIdentifier("schemaDirectory", true);
680    schemaPath.addLongIdentifier("schema-directory", true);
681    schemaPath.addLongIdentifier("schema", true);
682    parser.addArgument(schemaPath);
683
684
685    parser.addDependentArgumentSet(compressOutput, outputLDIF);
686    parser.addDependentArgumentSet(encryptOutput, outputLDIF);
687    parser.addDependentArgumentSet(outputEncryptionPassphraseFile, outputLDIF);
688    parser.addDependentArgumentSet(overwriteExistingOutputLDIF, outputLDIF);
689
690    parser.addDependentArgumentSet(outputEncryptionPassphraseFile,
691         encryptOutput);
692
693    parser.addExclusiveArgumentSet(includeAttribute, excludeAttribute);
694    parser.addExclusiveArgumentSet(includeAttribute,
695         includeOperationalAttributes);
696
697    parser.addExclusiveArgumentSet(includeFilter, excludeFilter);
698
699    parser.addDependentArgumentSet(excludeNoUserModificationAttributes,
700         includeOperationalAttributes);
701
702    parser.addExclusiveArgumentSet(nonReversibleModifications,
703         singleValueChanges);
704  }
705
706
707
708  /**
709   * {@inheritDoc}
710   */
711  @Override()
712  public void doExtendedArgumentValidation()
713         throws ArgumentException
714  {
715    // If the LDIF file exists and either compressOutput or encryptOutput is
716    // present, then the overwrite argument must also be present.
717    final File outputFile = outputLDIF.getValue();
718    if ((outputFile != null) && outputFile.exists() &&
719         (compressOutput.isPresent() || encryptOutput.isPresent()) &&
720         (! overwriteExistingOutputLDIF.isPresent()))
721    {
722      throw new ArgumentException(
723           ERR_LDIF_DIFF_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get(
724                compressOutput.getIdentifierString(),
725                encryptOutput.getIdentifierString(),
726                overwriteExistingOutputLDIF.getIdentifierString()));
727    }
728  }
729
730
731
732  /**
733   * {@inheritDoc}
734   */
735  @Override()
736  @NotNull()
737  public ResultCode doToolProcessing()
738  {
739    // Get the change types to use for processing.
740    final Set<ChangeType> changeTypes = EnumSet.noneOf(ChangeType.class);
741    for (final String value : changeType.getValues())
742    {
743      switch (StaticUtils.toLowerCase(value))
744      {
745        case CHANGE_TYPE_ADD:
746          changeTypes.add(ChangeType.ADD);
747          break;
748        case CHANGE_TYPE_DELETE:
749          changeTypes.add(ChangeType.DELETE);
750          break;
751        case CHANGE_TYPE_MODIFY:
752          changeTypes.add(ChangeType.MODIFY);
753          break;
754      }
755    }
756
757
758    // Get the schema to use when performing LDIF processing.
759    final Schema schema;
760    try
761    {
762      if (schemaPath.isPresent())
763      {
764        schema = getSchema(schemaPath.getValues());
765      }
766      else if (PING_SERVER_AVAILABLE)
767      {
768        schema = getSchema(Collections.singletonList(StaticUtils.constructPath(
769             PING_SERVER_ROOT, "config", "schema")));
770      }
771      else
772      {
773        schema = Schema.getDefaultStandardSchema();
774      }
775    }
776    catch (final Exception e)
777    {
778      Debug.debugException(e);
779      logCompletionMessage(true,
780           ERR_LDIF_DIFF_CANNOT_GET_SCHEMA.get(
781                StaticUtils.getExceptionMessage(e)));
782      return ResultCode.LOCAL_ERROR;
783    }
784
785
786    // Identify the sets of include and exclude attributes.
787    final Set<String> includeAttrs;
788    if (includeAttribute.isPresent())
789    {
790      final Set<String> s = new HashSet<>();
791      for (final String includeAttr : includeAttribute.getValues())
792      {
793        final String lowerName = StaticUtils.toLowerCase(includeAttr);
794        s.add(lowerName);
795
796        final AttributeTypeDefinition at = schema.getAttributeType(lowerName);
797        if (at != null)
798        {
799          s.add(StaticUtils.toLowerCase(at.getOID()));
800          for (final String name : at.getNames())
801          {
802            s.add(StaticUtils.toLowerCase(name));
803          }
804        }
805      }
806      includeAttrs = Collections.unmodifiableSet(s);
807    }
808    else
809    {
810      includeAttrs = Collections.emptySet();
811    }
812
813    final Set<String> excludeAttrs;
814    if (excludeAttribute.isPresent())
815    {
816      final Set<String> s = new HashSet<>();
817      for (final String excludeAttr : excludeAttribute.getValues())
818      {
819        final String lowerName = StaticUtils.toLowerCase(excludeAttr);
820        s.add(lowerName);
821
822        final AttributeTypeDefinition at = schema.getAttributeType(lowerName);
823        if (at != null)
824        {
825          s.add(StaticUtils.toLowerCase(at.getOID()));
826          for (final String name : at.getNames())
827          {
828            s.add(StaticUtils.toLowerCase(name));
829          }
830        }
831      }
832      excludeAttrs = Collections.unmodifiableSet(s);
833    }
834    else
835    {
836      excludeAttrs = Collections.emptySet();
837    }
838
839
840    // Read the source and target LDIF files into memory.
841    final TreeMap<DN,Entry> sourceEntries;
842    try
843    {
844      sourceEntries = readEntries(sourceLDIF.getValue(),
845           sourceEncryptionPassphraseFile.getValue(), schema);
846      out(INFO_LDIF_DIFF_READ_FROM_SOURCE_LDIF.get(
847           sourceLDIF.getValue().getName(), sourceEntries.size()));
848    }
849    catch (final LDAPException e)
850    {
851      Debug.debugException(e);
852      logCompletionMessage(true,
853           ERR_LDIF_DIFF_CANNOT_READ_SOURCE_LDIF.get(
854                sourceLDIF.getValue().getAbsolutePath(), e.getMessage()));
855      return e.getResultCode();
856    }
857
858    final TreeMap<DN,Entry> targetEntries;
859    try
860    {
861      targetEntries = readEntries(targetLDIF.getValue(),
862           targetEncryptionPassphraseFile.getValue(), schema);
863      out(INFO_LDIF_DIFF_READ_FROM_TARGET_LDIF.get(
864           targetLDIF.getValue().getName(), targetEntries.size()));
865      out();
866    }
867    catch (final LDAPException e)
868    {
869      Debug.debugException(e);
870      logCompletionMessage(true,
871           ERR_LDIF_DIFF_CANNOT_READ_TARGET_LDIF.get(
872                targetLDIF.getValue().getAbsolutePath(), e.getMessage()));
873      return e.getResultCode();
874    }
875
876
877    final String outputFilePath;
878    if (outputLDIF.isPresent())
879    {
880      outputFilePath = outputLDIF.getValue().getAbsolutePath();
881    }
882    else
883    {
884      outputFilePath = "{STDOUT}";
885    }
886
887
888    // Open the output file for writing.
889    long addCount = 0L;
890    long deleteCount = 0L;
891    long modifyCount = 0L;
892    try (OutputStream outputStream = openOutputStream();
893         LDIFWriter ldifWriter = new LDIFWriter(outputStream))
894    {
895      // First, identify any entries that have been added (that is, entries in
896      // the target set that are not in the source set), and write them to the
897      // output file.
898      if (changeTypes.contains(ChangeType.ADD))
899      {
900        try
901        {
902          addCount = writeAdds(sourceEntries, targetEntries, ldifWriter,
903               schema, includeAttrs, excludeAttrs);
904        }
905        catch (final LDAPException e)
906        {
907          Debug.debugException(e);
908          logCompletionMessage(true,
909               ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath,
910                    e.getMessage()));
911          return e.getResultCode();
912        }
913      }
914
915
916      // Next, identify any entries that have been modified (that is, entries
917      // that exist in both sets, but are different between those sets), and
918      // write them to the output file.  We'll write modifies after adds because
919      // that allows modifications to reference newly created entries, and we'll
920      // write modifies before deletes because that allows modifications to
921      // remove references to entries that will be removed.
922      if (changeTypes.contains(ChangeType.MODIFY))
923      {
924        try
925        {
926          modifyCount = writeModifications(sourceEntries, targetEntries,
927               ldifWriter, schema, includeAttrs, excludeAttrs);
928        }
929        catch (final LDAPException e)
930        {
931          Debug.debugException(e);
932          logCompletionMessage(true,
933               ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath,
934                    e.getMessage()));
935          return e.getResultCode();
936        }
937      }
938
939
940      // Finally, identify any deletes (entries that were only in the set of
941      // source entries) and write them to the output file.
942      if (changeTypes.contains(ChangeType.DELETE))
943      {
944        try
945        {
946          deleteCount = writeDeletes(sourceEntries, targetEntries, ldifWriter,
947               schema, includeAttrs, excludeAttrs);
948        }
949        catch (final LDAPException e)
950        {
951          Debug.debugException(e);
952          logCompletionMessage(true,
953               ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath,
954                    e.getMessage()));
955          return e.getResultCode();
956        }
957      }
958
959
960      // If we've gotten here, then everything was successful.
961      ldifWriter.flush();
962      logCompletionMessage(false, INFO_LDIF_DIFF_COMPLETED.get());
963      if (changeTypes.contains(ChangeType.ADD))
964      {
965        out(INFO_LDIF_DIFF_COMPLETED_ADD_COUNT.get(addCount));
966      }
967
968      if (changeTypes.contains(ChangeType.MODIFY))
969      {
970        out(INFO_LDIF_DIFF_COMPLETED_MODIFY_COUNT.get(modifyCount));
971      }
972
973      if (changeTypes.contains(ChangeType.DELETE))
974      {
975        out(INFO_LDIF_DIFF_COMPLETED_DELETE_COUNT.get(deleteCount));
976      }
977
978      return ResultCode.SUCCESS;
979    }
980    catch (final LDAPException e)
981    {
982      Debug.debugException(e);
983      logCompletionMessage(true,
984           ERR_LDIF_DIFF_CANNOT_OPEN_OUTPUT.get(outputFilePath,
985                e.getMessage()));
986      return e.getResultCode();
987    }
988    catch (final Exception e)
989    {
990      Debug.debugException(e);
991      logCompletionMessage(true,
992           ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath,
993                StaticUtils.getExceptionMessage(e)));
994      return ResultCode.LOCAL_ERROR;
995    }
996  }
997
998
999
1000  /**
1001   * Retrieves the schema contained in the specified paths.
1002   *
1003   * @param  paths  The paths to use to access the schema.
1004   *
1005   * @return  The schema read from the specified files.
1006   *
1007   * @throws  Exception  If a problem is encountered while loading the schema.
1008   */
1009  @NotNull()
1010  private static Schema getSchema(@NotNull final List<File> paths)
1011          throws Exception
1012  {
1013    final Set<File> schemaFiles = new LinkedHashSet<>();
1014    for (final File f : paths)
1015    {
1016      if (f.exists())
1017      {
1018        if (f.isFile())
1019        {
1020          schemaFiles.add(f);
1021        }
1022        else if (f.isDirectory())
1023        {
1024          final TreeMap<String,File> sortedFiles = new TreeMap<>();
1025          for (final File fileInDir : f.listFiles())
1026          {
1027            if (fileInDir.isFile())
1028            {
1029              sortedFiles.put(fileInDir.getName(), fileInDir);
1030            }
1031          }
1032
1033          schemaFiles.addAll(sortedFiles.values());
1034        }
1035      }
1036    }
1037
1038    return Schema.getSchema(new ArrayList<>(schemaFiles));
1039  }
1040
1041
1042
1043  /**
1044   * Reads all of the entries in the specified LDIF file into a map.
1045   *
1046   * @param  ldifFile   The path to the LDIF file to read.  It must not be
1047   *                    {@code null}.
1048   * @param  encPWFile  The path to the file containing the passphrase used to
1049   *                    encrypt the LDIF file.  It may be {@code null} if the
1050   *                    LDIF file is not encrypted, or if the encryption key is
1051   *                    to be obtained through an alternate means.
1052   * @param  schema     The schema to use when reading the LDIF file.  It must
1053   *                    not be {@code null}.
1054   *
1055   * @return  The map of entries read from the file.
1056   *
1057   * @throws  LDAPException  If a problem occurs while attempting to read the
1058   *                         entries.
1059   */
1060  @NotNull()
1061  private TreeMap<DN,Entry> readEntries(@NotNull final File ldifFile,
1062                                        @Nullable final File encPWFile,
1063                                        @NotNull final Schema schema)
1064          throws LDAPException
1065  {
1066    if (encPWFile != null)
1067    {
1068      try
1069      {
1070        addPassphrase(getPasswordFileReader().readPassword(encPWFile));
1071      }
1072      catch (final Exception e)
1073      {
1074        Debug.debugException(e);
1075        throw new LDAPException(ResultCode.LOCAL_ERROR,
1076             ERR_LDIF_DIFF_CANNOT_OPEN_PW_FILE.get(encPWFile.getAbsolutePath(),
1077                  StaticUtils.getExceptionMessage(e)),
1078             e);
1079      }
1080    }
1081
1082
1083    InputStream inputStream = null;
1084    try
1085    {
1086      try
1087      {
1088        inputStream = new FileInputStream(ldifFile);
1089      }
1090      catch (final Exception e)
1091      {
1092        Debug.debugException(e);
1093        throw new LDAPException(ResultCode.LOCAL_ERROR,
1094             ERR_LDIF_DIFF_CANNOT_OPEN_LDIF_FILE.get(
1095                  StaticUtils.getExceptionMessage(e)),
1096             e);
1097      }
1098
1099      try
1100      {
1101        final ObjectPair<InputStream,char[]> p =
1102             ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
1103                  encryptionPassphrases, (encPWFile == null),
1104                  INFO_LDIF_DIFF_PROMPT_FOR_ENC_PW.get(ldifFile.getName()),
1105                  ERR_LDIF_DIFF_PROMPT_WRONG_ENC_PW.get(), getOut(), getErr());
1106        inputStream = p.getFirst();
1107        addPassphrase(p.getSecond());
1108      }
1109      catch (final Exception e)
1110      {
1111        Debug.debugException(e);
1112        throw new LDAPException(ResultCode.LOCAL_ERROR,
1113             ERR_LDIF_DIFF_CANNOT_DECRYPT_LDIF_FILE.get(
1114                  StaticUtils.getExceptionMessage(e)),
1115             e);
1116      }
1117
1118      try
1119      {
1120        inputStream =
1121             ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
1122      }
1123      catch (final Exception e)
1124      {
1125        Debug.debugException(e);
1126        throw new LDAPException(ResultCode.LOCAL_ERROR,
1127             ERR_LDIF_DIFF_CANNOT_DECOMPRESS_LDIF_FILE.get(
1128                  StaticUtils.getExceptionMessage(e)),
1129             e);
1130      }
1131
1132      try (LDIFReader reader = new LDIFReader(inputStream))
1133      {
1134        reader.setSchema(schema);
1135        if (stripTrailingSpaces.isPresent())
1136        {
1137          reader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
1138        }
1139        else
1140        {
1141          reader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT);
1142        }
1143
1144        final TreeMap<DN,Entry> entryMap = new TreeMap<>();
1145        while (true)
1146        {
1147          final Entry entry = reader.readEntry();
1148          if (entry == null)
1149          {
1150            break;
1151          }
1152
1153          entryMap.put(entry.getParsedDN(), entry);
1154        }
1155
1156        return entryMap;
1157      }
1158      catch (final Exception e)
1159      {
1160        Debug.debugException(e);
1161        throw new LDAPException(ResultCode.LOCAL_ERROR,
1162             ERR_LDIF_DIFF_ERROR_READING_OR_DECODING.get(
1163                  StaticUtils.getExceptionMessage(e)),
1164             e);
1165      }
1166    }
1167    finally
1168    {
1169      if (inputStream != null)
1170      {
1171        try
1172        {
1173          inputStream.close();
1174        }
1175        catch (final Exception e)
1176        {
1177          Debug.debugException(e);
1178        }
1179      }
1180    }
1181  }
1182
1183
1184
1185  /**
1186   * Updates the list of encryption passphrases with the provided passphrase, if
1187   * it is not already present.
1188   *
1189   * @param  passphrase  The passphrase to be added.  It may optionally be
1190   *                     {@code null} (in which case no action will be taken).
1191   */
1192  private void addPassphrase(@Nullable final char[] passphrase)
1193  {
1194    if (passphrase == null)
1195    {
1196      return;
1197    }
1198
1199    for (final char[] existingPassphrase : encryptionPassphrases)
1200    {
1201      if (Arrays.equals(existingPassphrase, passphrase))
1202      {
1203        return;
1204      }
1205    }
1206
1207    encryptionPassphrases.add(passphrase);
1208  }
1209
1210
1211
1212  /**
1213   * Opens the output stream to use to write the identified differences.
1214   *
1215   * @return  The output stream that was opened.
1216   *
1217   * @throws  LDAPException  If a problem is encountered while opening the
1218   *                         output stream.
1219   */
1220  @NotNull()
1221  private OutputStream openOutputStream()
1222          throws LDAPException
1223  {
1224    if (! outputLDIF.isPresent())
1225    {
1226      return getOut();
1227    }
1228
1229    OutputStream outputStream = null;
1230    boolean closeOutputStream = true;
1231    try
1232    {
1233      try
1234      {
1235
1236        outputStream = new FileOutputStream(outputLDIF.getValue(),
1237             (! overwriteExistingOutputLDIF.isPresent()));
1238      }
1239      catch (final Exception e)
1240      {
1241        Debug.debugException(e);
1242        throw new LDAPException(ResultCode.LOCAL_ERROR,
1243             ERR_LDIF_DIFF_CANNOT_OPEN_OUTPUT_FILE.get(
1244                  StaticUtils.getExceptionMessage(e)),
1245             e);
1246      }
1247
1248      if (encryptOutput.isPresent())
1249      {
1250        try
1251        {
1252          final char[] passphrase;
1253          if (outputEncryptionPassphraseFile.isPresent())
1254          {
1255            passphrase = getPasswordFileReader().readPassword(
1256                 outputEncryptionPassphraseFile.getValue());
1257          }
1258          else
1259          {
1260            passphrase = ToolUtils.promptForEncryptionPassphrase(false, true,
1261                 INFO_LDIF_DIFF_PROMPT_OUTPUT_FILE_ENC_PW.get(),
1262                 INFO_LDIF_DIFF_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(),
1263                 getErr()).toCharArray();
1264          }
1265
1266          outputStream = new PassphraseEncryptedOutputStream(passphrase,
1267               outputStream, null, true, true);
1268        }
1269        catch (final Exception e)
1270        {
1271          Debug.debugException(e);
1272          throw new LDAPException(ResultCode.LOCAL_ERROR,
1273               ERR_LDIF_DIFF_CANNOT_ENCRYPT_OUTPUT_FILE.get(
1274                    StaticUtils.getExceptionMessage(e)),
1275               e);
1276        }
1277      }
1278
1279      if (compressOutput.isPresent())
1280      {
1281        try
1282        {
1283          outputStream = new GZIPOutputStream(outputStream);
1284        }
1285        catch (final Exception e)
1286        {
1287          Debug.debugException(e);
1288          throw new LDAPException(ResultCode.LOCAL_ERROR,
1289               ERR_LDIF_DIFF_CANNOT_COMPRESS_OUTPUT_FILE.get(
1290                    StaticUtils.getExceptionMessage(e)),
1291               e);
1292        }
1293      }
1294
1295      closeOutputStream = false;
1296      return outputStream;
1297    }
1298    finally
1299    {
1300      if (closeOutputStream && (outputStream != null))
1301      {
1302        try
1303        {
1304          outputStream.close();
1305        }
1306        catch (final Exception e)
1307        {
1308          Debug.debugException(e);
1309        }
1310      }
1311    }
1312  }
1313
1314
1315
1316  /**
1317   * Writes add change records for all entries contained in the given target
1318   * entry map that are not in the source entry map.
1319   *
1320   * @param  sourceEntries  The map of entries read from the source LDIF file.
1321   *                        It must not be {@code null}.
1322   * @param  targetEntries  The map of entries read from the target LDIF file.
1323   *                        It must not be {@code null}.
1324   * @param  writer         The LDIF writer to use to write any changes.  It
1325   *                        must not be {@ocde null} and it must be open.
1326   * @param  schema         The schema to use to identify operational
1327   *                        attributes.  It must not be {@ocde null}.
1328   * @param  includeAttrs   A set containing all names and OIDs for all
1329   *                        attribute types that should be included in the
1330   *                        entry.  It must not be {@ocde null} but may be
1331   *                        empty.  All values must be formatted entirely in
1332   *                        lowercase.
1333   * @param  excludeAttrs   A set containing all names and OIDs for all
1334   *                        attribute types that should be excluded from the
1335   *                        entry.  It must not be {@code null} but may be
1336   *                        empty.  All values must be formatted entirely in
1337   *                        lowercase.
1338   *
1339   * @return  The number of added entries that were identified during
1340   *          processing.
1341   *
1342   * @throws  LDAPException  If a problem is encountered while writing the ad
1343   *                         change records.
1344   */
1345  private long writeAdds(@NotNull final TreeMap<DN,Entry> sourceEntries,
1346                         @NotNull final TreeMap<DN,Entry> targetEntries,
1347                         @NotNull final LDIFWriter writer,
1348                         @NotNull final Schema schema,
1349                         @NotNull final Set<String> includeAttrs,
1350                         @NotNull final Set<String> excludeAttrs)
1351          throws LDAPException
1352  {
1353    long addCount = 0L;
1354
1355    for (final Map.Entry<DN,Entry> e : targetEntries.entrySet())
1356    {
1357      final DN entryDN = e.getKey();
1358      final Entry entry = e.getValue();
1359      if (! sourceEntries.containsKey(entryDN))
1360      {
1361        if (! includeEntryByFilter(schema, entry))
1362        {
1363          continue;
1364        }
1365
1366        final Entry paredEntry = pareEntry(entry, schema, includeAttrs,
1367             excludeAttrs);
1368        if (paredEntry == null)
1369        {
1370          continue;
1371        }
1372
1373        try
1374        {
1375          writer.writeChangeRecord(new LDIFAddChangeRecord(paredEntry),
1376               INFO_LDIF_DIFF_ADD_COMMENT.get());
1377          addCount++;
1378        }
1379        catch (final Exception ex)
1380        {
1381          Debug.debugException(ex);
1382        throw new LDAPException(ResultCode.LOCAL_ERROR,
1383             ERR_LDIF_DIFF_CANNOT_WRITE_ADD_FOR_ENTRY.get(entry.getDN(),
1384                  StaticUtils.getExceptionMessage(ex)),
1385             ex);
1386        }
1387      }
1388    }
1389
1390    return addCount;
1391  }
1392
1393
1394
1395  /**
1396   * Indicates whether the specified entry may be included in the output based
1397   * on the include filter and exclude filter configuration.
1398   *
1399   * @param  schema   The schema to use when making the determination.  It must
1400   *                  not be {@code null}.
1401   * @param  entries  The entries for which to make the determination.  It must
1402   *                  not be {@code null} or empty.
1403   *
1404   * @return  {@code true} if the entry should be included, or {@code false} if]
1405   *          not.
1406   */
1407  private boolean includeEntryByFilter(@NotNull final Schema schema,
1408                                       @NotNull final Entry... entries)
1409  {
1410    for (final Entry entry : entries)
1411    {
1412      for (final Filter f : excludeFilter.getValues())
1413      {
1414        try
1415        {
1416          if (f.matchesEntry(entry, schema))
1417          {
1418            return false;
1419          }
1420        }
1421        catch (final Exception ex)
1422        {
1423          Debug.debugException(ex);
1424        }
1425      }
1426    }
1427
1428    if (includeFilter.isPresent())
1429    {
1430      for (final Entry entry : entries)
1431      {
1432        for (final Filter f : includeFilter.getValues())
1433        {
1434          try
1435          {
1436            if (f.matchesEntry(entry, schema))
1437            {
1438              return true;
1439            }
1440          }
1441          catch (final Exception e)
1442          {
1443            Debug.debugException(e);
1444          }
1445        }
1446      }
1447
1448      return false;
1449    }
1450
1451    return true;
1452  }
1453
1454
1455
1456  /**
1457   * Creates a pared-down copy of the provided entry based on the requested
1458   * set of options.
1459   *
1460   * @param  entry         The entry to be pared down.  It must not be
1461   *                       {@code null}.
1462   * @param  schema        The schema to use during processing.  It must not be
1463   *                       {@ocde null}.
1464   * @param  includeAttrs  A set containing all names and OIDs for all attribute
1465   *                       types that should be included in the entry.  It must
1466   *                       not be {@ocde null} but may be empty.  All values
1467   *                       must be formatted entirely in lowercase.
1468   * @param  excludeAttrs  A set containing all names and OIDs for all attribute
1469   *                       types that should be excluded from the entry.  It
1470   *                       must not be {@code null} but may be empty.  All
1471   *                       values must be formatted entirely in lowercase.
1472   *
1473   * @return  A pared-down copy of the provided entry, or {@code null} if the
1474   *          pared-down entry would not include any attributes.
1475   */
1476  @Nullable()
1477  private Entry pareEntry(@NotNull final Entry entry,
1478                          @NotNull final Schema schema,
1479                          @NotNull final Set<String> includeAttrs,
1480                          @NotNull final Set<String> excludeAttrs)
1481  {
1482    final List<Attribute> paredAttributeList = new ArrayList<>();
1483    for (final Attribute a : entry.getAttributes())
1484    {
1485      final String baseName = StaticUtils.toLowerCase(a.getBaseName());
1486      if (excludeAttrs.contains(baseName))
1487      {
1488        continue;
1489      }
1490
1491      if ((! includeAttrs.isEmpty()) && (! includeAttrs.contains(baseName)))
1492      {
1493        continue;
1494      }
1495
1496      final AttributeTypeDefinition at = schema.getAttributeType(baseName);
1497      if ((at != null) && at.isOperational())
1498      {
1499        if (! includeOperationalAttributes.isPresent())
1500        {
1501          continue;
1502        }
1503
1504        if (at.isNoUserModification() &&
1505             excludeNoUserModificationAttributes.isPresent())
1506        {
1507          continue;
1508        }
1509      }
1510
1511      paredAttributeList.add(a);
1512    }
1513
1514    if (paredAttributeList.isEmpty())
1515    {
1516      return null;
1517    }
1518
1519    return new Entry(entry.getDN(), paredAttributeList);
1520  }
1521
1522
1523
1524  /**
1525   * Identifies entries that exist in both the source and target maps and
1526   * determines whether there are any changes between them.
1527   *
1528   * @param  sourceEntries  The map of entries read from the source LDIF file.
1529   *                        It must not be {@code null}.
1530   * @param  targetEntries  The map of entries read from the target LDIF file.
1531   *                        It must not be {@code null}.
1532   * @param  writer         The LDIF writer to use to write any changes.  It
1533   *                        must not be {@ocde null} and it must be open.
1534   * @param  schema         The schema to use to identify operational
1535   *                        attributes.  It must not be {@ocde null}.
1536   * @param  includeAttrs   A set containing all names and OIDs for all
1537   *                        attribute types that should be included in the
1538   *                        set of modifications.  It must not be {@ocde null}
1539   *                        but may be empty.  All values must be formatted
1540   *                        entirely in lowercase.
1541   * @param  excludeAttrs   A set containing all names and OIDs for all
1542   *                        attribute types that should be excluded from the
1543   *                        set of modifications.  It must not be {@ocde null}
1544   *                        but may be empty.  All values must be formatted
1545   *                        entirely in lowercase.
1546   *
1547   * @return  The number of modified entries that were identified during
1548   *          processing.
1549   *
1550   * @throws  LDAPException  If a problem is encountered while writing
1551   *                         modified entries.
1552   */
1553  private long writeModifications(
1554                    @NotNull final TreeMap<DN,Entry> sourceEntries,
1555                    @NotNull final TreeMap<DN,Entry> targetEntries,
1556                    @NotNull final LDIFWriter writer,
1557                    @NotNull final Schema schema,
1558                    @NotNull final Set<String> includeAttrs,
1559                    @NotNull final Set<String> excludeAttrs)
1560          throws LDAPException
1561  {
1562    long modCount = 0L;
1563
1564    for (final Map.Entry<DN,Entry> sourceMapEntry : sourceEntries.entrySet())
1565    {
1566      final DN sourceDN = sourceMapEntry.getKey();
1567
1568      final Entry targetEntry = targetEntries.get(sourceDN);
1569      if (targetEntry == null)
1570      {
1571        continue;
1572      }
1573
1574      final Entry sourceEntry = sourceMapEntry.getValue();
1575
1576      if (! includeEntryByFilter(schema, sourceEntry, targetEntry))
1577      {
1578        continue;
1579      }
1580
1581      final List<Modification> mods = Entry.diff(sourceEntry, targetEntry,
1582           false, (! nonReversibleModifications.isPresent()), true);
1583      if (writeModifiedEntry(sourceDN, mods, writer, schema, includeAttrs,
1584           excludeAttrs))
1585      {
1586        modCount++;
1587      }
1588    }
1589
1590    return modCount;
1591  }
1592
1593
1594
1595  /**
1596   * Writes a modified entry to the LDIF writer.
1597   *
1598   * @param  dn            The DN of the entry to write.  It must not be
1599   *                       {@code null}.
1600   * @param  mods          The modifications to be written.
1601   * @param  writer        The LDIF writer to use to write the modify change
1602   *                       record(s).
1603   * @param  schema        The schema to use to identify operational attributes.
1604   *                       It must not be {@code null}.
1605   * @param  includeAttrs  A set containing all names and OIDs for all attribute
1606   *                       types that should be included in the set of
1607   *                       modifications.  It must not be {@ocde null} but may
1608   *                       be empty.  All values must be formatted entirely in
1609   *                       lowercase.
1610   * @param  excludeAttrs  A set containing all names and OIDs for all attribute
1611   *                       types that should be excluded from the set of
1612   *                       modifications.  It must not be {@ocde null} but may
1613   *                       be empty.  All values must be formatted entirely in
1614   *                       lowercase.
1615   *
1616   * @return  {@code true} if one or more modify change records were written, or
1617   *          {@code false} if not.
1618   *
1619   * @throws  LDAPException  If a problem occurs while trying to write the
1620   *                         modifications.
1621   */
1622  private boolean writeModifiedEntry(@NotNull final DN dn,
1623                                     @NotNull final List<Modification> mods,
1624                                     @NotNull final LDIFWriter writer,
1625                                     @NotNull final Schema schema,
1626                                     @NotNull final Set<String> includeAttrs,
1627                                     @NotNull final Set<String> excludeAttrs)
1628          throws LDAPException
1629  {
1630    if (mods.isEmpty())
1631    {
1632      return false;
1633    }
1634
1635    final List<Modification> paredMods = new ArrayList<>(mods.size());
1636    for (final Modification m : mods)
1637    {
1638      final Attribute a = m.getAttribute();
1639      final String baseName = StaticUtils.toLowerCase(a.getBaseName());
1640      if (excludeAttrs.contains(baseName))
1641      {
1642        continue;
1643      }
1644
1645      if ((! includeAttrs.isEmpty()) && ! includeAttrs.contains(baseName))
1646      {
1647        continue;
1648      }
1649
1650      final AttributeTypeDefinition at =
1651           schema.getAttributeType(a.getBaseName());
1652      if ((at != null) && at.isOperational())
1653      {
1654        if (includeOperationalAttributes.isPresent())
1655        {
1656          if (at.isNoUserModification())
1657          {
1658            if (! excludeNoUserModificationAttributes.isPresent())
1659            {
1660              paredMods.add(m);
1661            }
1662          }
1663          else
1664          {
1665            paredMods.add(m);
1666          }
1667        }
1668      }
1669      else
1670      {
1671        paredMods.add(m);
1672      }
1673    }
1674
1675    if (paredMods.isEmpty())
1676    {
1677      return false;
1678    }
1679
1680    try
1681    {
1682      if (singleValueChanges.isPresent())
1683      {
1684        for (final Modification m : paredMods)
1685        {
1686          final Attribute a = m.getAttribute();
1687          if (a.size() > 1)
1688          {
1689            for (final byte[] value : a.getValueByteArrays())
1690            {
1691              writer.writeChangeRecord(new LDIFModifyChangeRecord(dn.toString(),
1692                   new Modification(m.getModificationType(),
1693                        m.getAttributeName(), value)));
1694            }
1695          }
1696          else
1697          {
1698            writer.writeChangeRecord(new LDIFModifyChangeRecord(dn.toString(),
1699                 m));
1700          }
1701        }
1702      }
1703      else
1704      {
1705        writer.writeChangeRecord(
1706             new LDIFModifyChangeRecord(dn.toString(), paredMods),
1707             INFO_LDIF_DIFF_MODIFY_COMMENT.get());
1708      }
1709    }
1710    catch (final Exception e)
1711    {
1712      Debug.debugException(e);
1713      throw new LDAPException(ResultCode.LOCAL_ERROR,
1714           ERR_LDIF_DIFF_CANNOT_WRITE_MODS_TO_ENTRY.get(dn.toString(),
1715                StaticUtils.getExceptionMessage(e)),
1716           e);
1717    }
1718
1719    return true;
1720  }
1721
1722
1723
1724  /**
1725   * Writes delete change records for all entries contained in the given source
1726   * entry map that are not in the target entry map.
1727   *
1728   * @param  sourceEntries  The map of entries read from the source LDIF file.
1729   *                        It must not be {@code null}.
1730   * @param  targetEntries  The map of entries read from the target LDIF file.
1731   *                        It must not be {@code null}.
1732   * @param  writer         The LDIF writer to use to write any changes.  It
1733   *                        must not be {@ocde null} and it must be open.
1734   * @param  schema         The schema to use to identify operational
1735   *                        attributes.  It must not be {@ocde null}.
1736   * @param  includeAttrs   A set containing all names and OIDs for all
1737   *                        attribute types that should be included in the
1738   *                        entry.  It must not be {@ocde null} but may be
1739   *                        empty.  All values must be formatted entirely in
1740   *                        lowercase.
1741   * @param  excludeAttrs   A set containing all names and OIDs for all
1742   *                        attribute types that should be excluded from the
1743   *                        entry.  It must not be {@code null} but may be
1744   *                        empty.  All values must be formatted entirely in
1745   *                        lowercase.
1746   *
1747   * @return  The number of deleted entries that were identified during
1748   *          processing.
1749   *
1750   * @throws  LDAPException  If a problem is encountered while writing the
1751   *                         delete change records.
1752   */
1753  private long writeDeletes(@NotNull final TreeMap<DN,Entry> sourceEntries,
1754                            @NotNull final TreeMap<DN,Entry> targetEntries,
1755                            @NotNull final LDIFWriter writer,
1756                            @NotNull final Schema schema,
1757                            @NotNull final Set<String> includeAttrs,
1758                            @NotNull final Set<String> excludeAttrs)
1759          throws LDAPException
1760  {
1761    long deleteCount = 0L;
1762
1763    for (final Map.Entry<DN,Entry> e : sourceEntries.descendingMap().entrySet())
1764    {
1765      final DN entryDN = e.getKey();
1766      final Entry entry = e.getValue();
1767      if (! targetEntries.containsKey(entryDN))
1768      {
1769        if (! includeEntryByFilter(schema, entry))
1770        {
1771          continue;
1772        }
1773
1774        final Entry paredEntry = pareEntry(entry, schema, includeAttrs,
1775             excludeAttrs);
1776        if (paredEntry == null)
1777        {
1778          continue;
1779        }
1780
1781        try
1782        {
1783          final String comment = INFO_LDIF_DIFF_DELETE_COMMENT.get() +
1784               StaticUtils.EOL + paredEntry.toLDIFString(75);
1785          writer.writeChangeRecord(
1786               new LDIFDeleteChangeRecord(paredEntry.getDN()), comment);
1787          deleteCount++;
1788        }
1789        catch (final Exception ex)
1790        {
1791          Debug.debugException(ex);
1792        throw new LDAPException(ResultCode.LOCAL_ERROR,
1793             ERR_LDIF_DIFF_CANNOT_WRITE_DELETE_FOR_ENTRY.get(entry.getDN(),
1794                  StaticUtils.getExceptionMessage(ex)),
1795             ex);
1796        }
1797      }
1798    }
1799
1800    return deleteCount;
1801  }
1802
1803
1804
1805  /**
1806   * Writes the provided message and sets it as the completion message.
1807   *
1808   * @param  isError  Indicates whether the message should be written to
1809   *                  standard error rather than standard output.
1810   * @param  message  The message to be written.
1811   */
1812  private void logCompletionMessage(final boolean isError,
1813                                    @NotNull final String message)
1814  {
1815    completionMessage.compareAndSet(null, message);
1816
1817    if (isError)
1818    {
1819      wrapErr(0, WRAP_COLUMN, message);
1820    }
1821    else
1822    {
1823      wrapOut(0, WRAP_COLUMN, message);
1824    }
1825  }
1826
1827
1828
1829  /**
1830   * {@inheritDoc}
1831   */
1832  @Override()
1833  @NotNull()
1834  public LinkedHashMap<String[],String> getExampleUsages()
1835  {
1836    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1837
1838    examples.put(
1839         new String[]
1840         {
1841           "--sourceLDIF", "actual.ldif",
1842           "--targetLDIF", "desired.ldif",
1843           "--outputLDIF", "diff.ldif"
1844         },
1845         INFO_LDIF_DIFF_EXAMPLE_1.get());
1846
1847    examples.put(
1848         new String[]
1849         {
1850           "--sourceLDIF", "actual.ldif",
1851           "--targetLDIF", "desired.ldif",
1852           "--outputLDIF", "diff.ldif",
1853           "--includeOperationalAttributes",
1854           "--excludeNoUserModificationAttributes",
1855           "--nonReversibleModifications"
1856         },
1857         INFO_LDIF_DIFF_EXAMPLE_2.get());
1858
1859    return examples;
1860  }
1861}