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