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