001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2013-2015 ForgeRock AS.
026 */
027package org.opends.server.tools;
028
029import java.io.BufferedReader;
030import java.io.FileReader;
031import java.io.IOException;
032import java.io.OutputStream;
033import java.io.PrintStream;
034import java.util.Collection;
035import java.util.HashSet;
036import java.util.Iterator;
037import java.util.LinkedHashSet;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.ListIterator;
041import java.util.TreeMap;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.opends.server.core.DirectoryServer;
045import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
046import org.opends.server.extensions.ConfigFileHandler;
047import org.opends.server.loggers.JDKLogging;
048import org.opends.server.types.Attribute;
049import org.opends.server.types.AttributeBuilder;
050import org.opends.server.types.AttributeType;
051import org.forgerock.opendj.ldap.ByteString;
052import org.opends.server.types.DN;
053import org.opends.server.types.DirectoryException;
054import org.opends.server.types.Entry;
055import org.opends.server.types.ExistingFileBehavior;
056import org.opends.server.types.LDIFExportConfig;
057import org.opends.server.types.LDIFImportConfig;
058import org.opends.server.types.Modification;
059import org.forgerock.opendj.ldap.ModificationType;
060import org.opends.server.types.NullOutputStream;
061import org.opends.server.types.ObjectClass;
062import org.opends.server.util.LDIFReader;
063import org.opends.server.util.LDIFWriter;
064import org.opends.server.util.StaticUtils;
065
066import com.forgerock.opendj.cli.ArgumentException;
067import com.forgerock.opendj.cli.ArgumentParser;
068import com.forgerock.opendj.cli.BooleanArgument;
069import com.forgerock.opendj.cli.CommonArguments;
070import com.forgerock.opendj.cli.StringArgument;
071
072import static org.opends.messages.ToolMessages.*;
073import static org.opends.server.protocols.ldap.LDAPResultCode.*;
074
075import static com.forgerock.opendj.cli.ArgumentConstants.*;
076
077import static org.opends.server.util.CollectionUtils.*;
078import static org.opends.server.util.ServerConstants.*;
079
080import static com.forgerock.opendj.cli.Utils.*;
081
082/**
083 * This class provides a program that may be used to determine the differences
084 * between two LDIF files, generating the output in LDIF change format.  There
085 * are several things to note about the operation of this program:
086 * <BR>
087 * <UL>
088 *   <LI>This program is only designed for cases in which both LDIF files to be
089 *       compared will fit entirely in memory at the same time.</LI>
090 *   <LI>This program will only compare live data in the LDIF files and will
091 *       ignore comments and other elements that do not have any real impact on
092 *       the way that the data is interpreted.</LI>
093 *   <LI>The differences will be generated in such a way as to provide the
094 *       maximum amount of information, so that there will be enough information
095 *       for the changes to be reversed (i.e., it will not use the "replace"
096 *       modification type but only the "add" and "delete" types, and contents
097 *       of deleted entries will be included as comments).</LI>
098 * </UL>
099 *
100 *
101 * Note
102 * that this is only an option for cases in which both LDIF files can fit in
103 * memory.  Also note that this will only compare live data in the LDIF files
104 * and will ignore comments and other elements that do not have any real impact
105 * on the way that the data is interpreted.
106 */
107public class LDIFDiff
108{
109  /**
110   * The fully-qualified name of this class.
111   */
112  private static final String CLASS_NAME = "org.opends.server.tools.LDIFDiff";
113
114
115
116  /**
117   * Provides the command line arguments to the <CODE>mainDiff</CODE> method
118   * so that they can be processed.
119   *
120   * @param  args  The command line arguments provided to this program.
121   */
122  public static void main(String[] args)
123  {
124    int exitCode = mainDiff(args, false, System.out, System.err);
125    if (exitCode != 0)
126    {
127      System.exit(filterExitCode(exitCode));
128    }
129  }
130
131
132
133  /**
134   * Parses the provided command line arguments and performs the appropriate
135   * LDIF diff operation.
136   *
137   * @param  args               The command line arguments provided to this
138   *                            program.
139   * @param  serverInitialized  Indicates whether the Directory Server has
140   *                            already been initialized (and therefore should
141   *                            not be initialized a second time).
142   * @param  outStream          The output stream to use for standard output, or
143   *                            {@code null} if standard output is not needed.
144   * @param  errStream          The output stream to use for standard error, or
145   *                            {@code null} if standard error is not needed.
146   *
147   * @return  The return code for this operation.  A value of zero indicates
148   *          that all processing completed successfully.  A nonzero value
149   *          indicates that some problem occurred during processing.
150   */
151  public static int mainDiff(String[] args, boolean serverInitialized,
152                             OutputStream outStream, OutputStream errStream)
153  {
154    PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
155    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
156    JDKLogging.disableLogging();
157
158    BooleanArgument overwriteExisting;
159    BooleanArgument showUsage;
160    BooleanArgument useCompareResultCode;
161    BooleanArgument singleValueChanges;
162    BooleanArgument doCheckSchema;
163    StringArgument  configClass;
164    StringArgument  configFile;
165    StringArgument  outputLDIF;
166    StringArgument  sourceLDIF;
167    StringArgument  targetLDIF;
168    StringArgument  ignoreAttrsFile;
169    StringArgument  ignoreEntriesFile;
170
171
172    LocalizableMessage toolDescription = INFO_LDIFDIFF_TOOL_DESCRIPTION.get();
173    ArgumentParser argParser = new ArgumentParser(CLASS_NAME, toolDescription,
174                                                  false);
175    argParser.setShortToolDescription(REF_SHORT_DESC_LDIFDIFF.get());
176    argParser.setVersionHandler(new DirectoryServerVersionHandler());
177    try
178    {
179      sourceLDIF = new StringArgument(
180              "sourceldif", 's', "sourceLDIF", true,
181              false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
182              INFO_LDIFDIFF_DESCRIPTION_SOURCE_LDIF.get());
183      argParser.addArgument(sourceLDIF);
184
185      targetLDIF = new StringArgument(
186              "targetldif", 't', "targetLDIF", true,
187              false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
188              INFO_LDIFDIFF_DESCRIPTION_TARGET_LDIF.get());
189      argParser.addArgument(targetLDIF);
190
191      outputLDIF = new StringArgument(
192              "outputldif", 'o', "outputLDIF", false,
193              false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
194              INFO_LDIFDIFF_DESCRIPTION_OUTPUT_LDIF.get());
195      argParser.addArgument(outputLDIF);
196
197      ignoreAttrsFile = new StringArgument(
198              "ignoreattrs", 'a', "ignoreAttrs", false,
199              false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
200              INFO_LDIFDIFF_DESCRIPTION_IGNORE_ATTRS.get());
201      argParser.addArgument(ignoreAttrsFile);
202
203      ignoreEntriesFile = new StringArgument(
204              "ignoreentries", 'e', "ignoreEntries", false,
205              false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
206              INFO_LDIFDIFF_DESCRIPTION_IGNORE_ENTRIES.get());
207      argParser.addArgument(ignoreEntriesFile);
208
209      overwriteExisting =
210           new BooleanArgument(
211                   "overwriteexisting", 'O',
212                   "overwriteExisting",
213                   INFO_LDIFDIFF_DESCRIPTION_OVERWRITE_EXISTING.get());
214      argParser.addArgument(overwriteExisting);
215
216      singleValueChanges =
217           new BooleanArgument(
218                   "singlevaluechanges", 'S', "singleValueChanges",
219                   INFO_LDIFDIFF_DESCRIPTION_SINGLE_VALUE_CHANGES.get());
220      argParser.addArgument(singleValueChanges);
221
222      doCheckSchema =
223        new BooleanArgument(
224                "checkschema", null, "checkSchema",
225                INFO_LDIFDIFF_DESCRIPTION_CHECK_SCHEMA.get());
226      argParser.addArgument(doCheckSchema);
227
228      configFile = new StringArgument("configfile", 'c', "configFile", false,
229                                      false, true,
230                                      INFO_CONFIGFILE_PLACEHOLDER.get(), null,
231                                      null,
232                                      INFO_DESCRIPTION_CONFIG_FILE.get());
233      configFile.setHidden(true);
234      argParser.addArgument(configFile);
235
236      configClass = new StringArgument("configclass", OPTION_SHORT_CONFIG_CLASS,
237                             OPTION_LONG_CONFIG_CLASS, false,
238                             false, true, INFO_CONFIGCLASS_PLACEHOLDER.get(),
239                             ConfigFileHandler.class.getName(), null,
240                             INFO_DESCRIPTION_CONFIG_CLASS.get());
241      configClass.setHidden(true);
242      argParser.addArgument(configClass);
243
244      showUsage =  CommonArguments.getShowUsage();
245      argParser.addArgument(showUsage);
246
247      useCompareResultCode =
248          new BooleanArgument("usecompareresultcode", 'r',
249              "useCompareResultCode",
250              INFO_LDIFDIFF_DESCRIPTION_USE_COMPARE_RESULT.get());
251      argParser.addArgument(useCompareResultCode);
252
253      argParser.setUsageArgument(showUsage);
254    }
255    catch (ArgumentException ae)
256    {
257      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
258      return OPERATIONS_ERROR;
259    }
260
261
262    // Parse the command-line arguments provided to the program.
263    try
264    {
265      argParser.parseArguments(args);
266    }
267    catch (ArgumentException ae)
268    {
269      argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
270      return CLIENT_SIDE_PARAM_ERROR;
271    }
272
273
274    // If we should just display usage or version information,
275    // then print it and exit.
276    if (argParser.usageOrVersionDisplayed())
277    {
278      return SUCCESS;
279    }
280
281    if (doCheckSchema.isPresent() && !configFile.isPresent())
282    {
283      String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
284      if (scriptName == null)
285      {
286        scriptName = "ldif-diff";
287      }
288      LocalizableMessage message = WARN_LDIFDIFF_NO_CONFIG_FILE.get(scriptName);
289      err.println(message);
290    }
291
292
293    boolean checkSchema = configFile.isPresent() && doCheckSchema.isPresent();
294    if (! serverInitialized)
295    {
296      // Bootstrap the Directory Server configuration for use as a client.
297      DirectoryServer directoryServer = DirectoryServer.getInstance();
298      DirectoryServer.bootstrapClient();
299
300
301      // If we're to use the configuration then initialize it, along with the
302      // schema.
303      if (checkSchema)
304      {
305        try
306        {
307          DirectoryServer.initializeJMX();
308        }
309        catch (Exception e)
310        {
311          printWrappedText(err, ERR_LDIFDIFF_CANNOT_INITIALIZE_JMX.get(configFile.getValue(), e.getMessage()));
312          return OPERATIONS_ERROR;
313        }
314
315        try
316        {
317          directoryServer.initializeConfiguration(configClass.getValue(),
318                                                  configFile.getValue());
319        }
320        catch (Exception e)
321        {
322          printWrappedText(err, ERR_LDIFDIFF_CANNOT_INITIALIZE_CONFIG.get(configFile.getValue(), e.getMessage()));
323          return OPERATIONS_ERROR;
324        }
325
326        try
327        {
328          directoryServer.initializeSchema();
329        }
330        catch (Exception e)
331        {
332          printWrappedText(err, ERR_LDIFDIFF_CANNOT_INITIALIZE_SCHEMA.get(configFile.getValue(), e.getMessage()));
333          return OPERATIONS_ERROR;
334        }
335      }
336    }
337
338    // Read in ignored entries and attributes if any
339    BufferedReader ignReader = null;
340    Collection<DN> ignoreEntries = new HashSet<>();
341    Collection<String> ignoreAttrs = new HashSet<>();
342
343    if (ignoreAttrsFile.getValue() != null)
344    {
345      try
346      {
347        ignReader = new BufferedReader(
348          new FileReader(ignoreAttrsFile.getValue()));
349        String line = null;
350        while ((line = ignReader.readLine()) != null)
351        {
352          ignoreAttrs.add(line.toLowerCase());
353        }
354        ignReader.close();
355      }
356      catch (Exception e)
357      {
358        printWrappedText(err, ERR_LDIFDIFF_CANNOT_READ_FILE_IGNORE_ATTRIBS.get(ignoreAttrsFile.getValue(), e));
359        return OPERATIONS_ERROR;
360      }
361      finally
362      {
363        StaticUtils.close(ignReader);
364      }
365    }
366
367    if (ignoreEntriesFile.getValue() != null)
368    {
369      try
370      {
371        ignReader = new BufferedReader(
372          new FileReader(ignoreEntriesFile.getValue()));
373        String line = null;
374        while ((line = ignReader.readLine()) != null)
375        {
376          try
377          {
378            DN dn = DN.valueOf(line);
379            ignoreEntries.add(dn);
380          }
381          catch (DirectoryException e)
382          {
383            LocalizableMessage message = INFO_LDIFDIFF_CANNOT_PARSE_STRING_AS_DN.get(
384                    line, ignoreEntriesFile.getValue());
385            err.println(message);
386          }
387        }
388        ignReader.close();
389      }
390      catch (Exception e)
391      {
392        printWrappedText(err, ERR_LDIFDIFF_CANNOT_READ_FILE_IGNORE_ENTRIES.get(ignoreEntriesFile.getValue(), e));
393        return OPERATIONS_ERROR;
394      }
395      finally
396      {
397        StaticUtils.close(ignReader);
398      }
399    }
400
401    // Open the source LDIF file and read it into a tree map.
402    LDIFReader reader;
403    LDIFImportConfig importConfig = new LDIFImportConfig(sourceLDIF.getValue());
404    try
405    {
406      reader = new LDIFReader(importConfig);
407    }
408    catch (Exception e)
409    {
410      printWrappedText(err, ERR_LDIFDIFF_CANNOT_OPEN_SOURCE_LDIF.get(sourceLDIF.getValue(), e));
411      return OPERATIONS_ERROR;
412    }
413
414    TreeMap<DN,Entry> sourceMap = new TreeMap<>();
415    try
416    {
417      while (true)
418      {
419        Entry entry = reader.readEntry(checkSchema);
420        if (entry == null)
421        {
422          break;
423        }
424
425        if (! ignoreEntries.contains(entry.getName()))
426        {
427          sourceMap.put(entry.getName(), entry);
428        }
429      }
430    }
431    catch (Exception e)
432    {
433      printWrappedText(err, ERR_LDIFDIFF_ERROR_READING_SOURCE_LDIF.get(sourceLDIF.getValue(), e));
434      return OPERATIONS_ERROR;
435    }
436    finally
437    {
438      StaticUtils.close(reader);
439    }
440
441
442    // Open the target LDIF file and read it into a tree map.
443    importConfig = new LDIFImportConfig(targetLDIF.getValue());
444    try
445    {
446      reader = new LDIFReader(importConfig);
447    }
448    catch (Exception e)
449    {
450      printWrappedText(err, ERR_LDIFDIFF_CANNOT_OPEN_TARGET_LDIF.get(targetLDIF.getValue(), e));
451      return OPERATIONS_ERROR;
452    }
453
454    TreeMap<DN,Entry> targetMap = new TreeMap<>();
455    try
456    {
457      while (true)
458      {
459        Entry entry = reader.readEntry(checkSchema);
460        if (entry == null)
461        {
462          break;
463        }
464
465        if (! ignoreEntries.contains(entry.getName()))
466        {
467          targetMap.put(entry.getName(), entry);
468        }
469      }
470    }
471    catch (Exception e)
472    {
473      printWrappedText(err, ERR_LDIFDIFF_ERROR_READING_TARGET_LDIF.get(targetLDIF.getValue(), e));
474      return OPERATIONS_ERROR;
475    }
476    finally
477    {
478      StaticUtils.close(reader);
479    }
480
481
482    // Open the output writer that we'll use to write the differences.
483    LDIFWriter writer;
484    try
485    {
486      LDIFExportConfig exportConfig;
487      if (outputLDIF.isPresent())
488      {
489        if (overwriteExisting.isPresent())
490        {
491          exportConfig = new LDIFExportConfig(outputLDIF.getValue(),
492                                              ExistingFileBehavior.OVERWRITE);
493        }
494        else
495        {
496          exportConfig = new LDIFExportConfig(outputLDIF.getValue(),
497                                              ExistingFileBehavior.APPEND);
498        }
499      }
500      else
501      {
502        exportConfig = new LDIFExportConfig(out);
503      }
504
505      writer = new LDIFWriter(exportConfig);
506    }
507    catch (Exception e)
508    {
509      printWrappedText(err, ERR_LDIFDIFF_CANNOT_OPEN_OUTPUT.get(e));
510      return OPERATIONS_ERROR;
511    }
512
513
514    try
515    {
516      boolean differenceFound;
517
518      // Check to see if either or both of the source and target maps are empty.
519      if (sourceMap.isEmpty())
520      {
521        if (targetMap.isEmpty())
522        {
523          // They're both empty, so there are no differences.
524          differenceFound = false;
525        }
526        else
527        {
528          // The target isn't empty, so they're all adds.
529          Iterator<DN> targetIterator = targetMap.keySet().iterator();
530          while (targetIterator.hasNext())
531          {
532            writeAdd(writer, targetMap.get(targetIterator.next()));
533          }
534          differenceFound = true;
535        }
536      }
537      else if (targetMap.isEmpty())
538      {
539        // The source isn't empty, so they're all deletes.
540        Iterator<DN> sourceIterator = sourceMap.keySet().iterator();
541        while (sourceIterator.hasNext())
542        {
543          writeDelete(writer, sourceMap.get(sourceIterator.next()));
544        }
545        differenceFound = true;
546      }
547      else
548      {
549        differenceFound = false;
550        // Iterate through all the entries in the source and target maps and
551        // identify the differences.
552        Iterator<DN> sourceIterator  = sourceMap.keySet().iterator();
553        Iterator<DN> targetIterator  = targetMap.keySet().iterator();
554        DN           sourceDN        = sourceIterator.next();
555        DN           targetDN        = targetIterator.next();
556        Entry        sourceEntry     = sourceMap.get(sourceDN);
557        Entry        targetEntry     = targetMap.get(targetDN);
558
559        while (true)
560        {
561          // Compare the DNs to determine the relative order of the
562          // entries.
563          int comparatorValue = sourceDN.compareTo(targetDN);
564          if (comparatorValue < 0)
565          {
566            // The source entry should be before the target entry, which means
567            // that the source entry has been deleted.
568            writeDelete(writer, sourceEntry);
569            differenceFound = true;
570            if (sourceIterator.hasNext())
571            {
572              sourceDN    = sourceIterator.next();
573              sourceEntry = sourceMap.get(sourceDN);
574            }
575            else
576            {
577              // There are no more source entries, so if there are more target
578              // entries then they're all adds.
579              writeAdd(writer, targetEntry);
580
581              while (targetIterator.hasNext())
582              {
583                targetDN    = targetIterator.next();
584                targetEntry = targetMap.get(targetDN);
585                writeAdd(writer, targetEntry);
586                differenceFound = true;
587              }
588
589              break;
590            }
591          }
592          else if (comparatorValue > 0)
593          {
594            // The target entry should be before the source entry, which means
595            // that the target entry has been added.
596            writeAdd(writer, targetEntry);
597            differenceFound = true;
598            if (targetIterator.hasNext())
599            {
600              targetDN    = targetIterator.next();
601              targetEntry = targetMap.get(targetDN);
602            }
603            else
604            {
605              // There are no more target entries so all of the remaining source
606              // entries are deletes.
607              writeDelete(writer, sourceEntry);
608              differenceFound = true;
609              while (sourceIterator.hasNext())
610              {
611                sourceDN = sourceIterator.next();
612                sourceEntry = sourceMap.get(sourceDN);
613                writeDelete(writer, sourceEntry);
614              }
615
616              break;
617            }
618          }
619          else
620          {
621            // The DNs are the same, so check to see if the entries are the
622            // same or have been modified.
623            if (writeModify(writer, sourceEntry, targetEntry, ignoreAttrs,
624                            singleValueChanges.isPresent()))
625            {
626              differenceFound = true;
627            }
628
629            if (sourceIterator.hasNext())
630            {
631              sourceDN    = sourceIterator.next();
632              sourceEntry = sourceMap.get(sourceDN);
633            }
634            else
635            {
636              // There are no more source entries, so if there are more target
637              // entries then they're all adds.
638              while (targetIterator.hasNext())
639              {
640                targetDN    = targetIterator.next();
641                targetEntry = targetMap.get(targetDN);
642                writeAdd(writer, targetEntry);
643                differenceFound = true;
644              }
645
646              break;
647            }
648
649            if (targetIterator.hasNext())
650            {
651              targetDN    = targetIterator.next();
652              targetEntry = targetMap.get(targetDN);
653            }
654            else
655            {
656              // There are no more target entries so all of the remaining source
657              // entries are deletes.
658              writeDelete(writer, sourceEntry);
659              differenceFound = true;
660              while (sourceIterator.hasNext())
661              {
662                sourceDN = sourceIterator.next();
663                sourceEntry = sourceMap.get(sourceDN);
664                writeDelete(writer, sourceEntry);
665              }
666
667              break;
668            }
669          }
670        }
671      }
672
673      if (!differenceFound)
674      {
675        LocalizableMessage message = INFO_LDIFDIFF_NO_DIFFERENCES.get();
676        writer.writeComment(message, 0);
677      }
678      if (useCompareResultCode.isPresent())
679      {
680        return !differenceFound ? COMPARE_TRUE : COMPARE_FALSE;
681      }
682    }
683    catch (IOException e)
684    {
685      printWrappedText(err, ERR_LDIFDIFF_ERROR_WRITING_OUTPUT.get(e));
686      return OPERATIONS_ERROR;
687    }
688    finally
689    {
690      StaticUtils.close(writer);
691    }
692
693
694    // If we've gotten to this point, then everything was successful.
695    return SUCCESS;
696  }
697
698
699
700  /**
701   * Writes an add change record to the LDIF writer.
702   *
703   * @param  writer  The writer to which the add record should be written.
704   * @param  entry   The entry that has been added.
705   *
706   * @throws  IOException  If a problem occurs while attempting to write the add
707   *                       record.
708   */
709  private static void writeAdd(LDIFWriter writer, Entry entry)
710          throws IOException
711  {
712    writer.writeAddChangeRecord(entry);
713    writer.flush();
714  }
715
716
717
718  /**
719   * Writes a delete change record to the LDIF writer, including a comment
720   * with the contents of the deleted entry.
721   *
722   * @param  writer  The writer to which the delete record should be written.
723   * @param  entry   The entry that has been deleted.
724   *
725   * @throws  IOException  If a problem occurs while attempting to write the
726   *                       delete record.
727   */
728  private static void writeDelete(LDIFWriter writer, Entry entry)
729          throws IOException
730  {
731    writer.writeDeleteChangeRecord(entry, true);
732    writer.flush();
733  }
734
735
736
737  /**
738   * Writes a modify change record to the LDIF writer.  Note that this will
739   * handle all the necessary logic for determining if the entries are actually
740   * different, and if they are the same then no output will be generated.  Also
741   * note that this will only look at differences between the objectclasses and
742   * user attributes.  It will ignore differences in the DN and operational
743   * attributes.
744   *
745   * @param  writer              The writer to which the modify record should be
746   *                             written.
747   * @param  sourceEntry         The source form of the entry.
748   * @param  targetEntry         The target form of the entry.
749   * @param  ignoreAttrs         Attributes that are ignored while calculating
750   *                             the differences.
751   * @param  singleValueChanges  Indicates whether each attribute-level change
752   *                             should be written in a separate modification
753   *                             per attribute value.
754   *
755   * @return  <CODE>true</CODE> if there were any differences found between the
756   *          source and target entries, or <CODE>false</CODE> if not.
757   *
758   * @throws  IOException  If a problem occurs while attempting to write the
759   *                       change record.
760   */
761  private static boolean writeModify(LDIFWriter writer, Entry sourceEntry,
762      Entry targetEntry, Collection<String> ignoreAttrs, boolean singleValueChanges)
763          throws IOException
764  {
765    // Create a list to hold the modifications that are found.
766    LinkedList<Modification> modifications = new LinkedList<>();
767
768
769    // Look at the set of objectclasses for the entries.
770    LinkedHashSet<ObjectClass> sourceClasses = new LinkedHashSet<>(sourceEntry.getObjectClasses().keySet());
771    LinkedHashSet<ObjectClass> targetClasses = new LinkedHashSet<>(targetEntry.getObjectClasses().keySet());
772    Iterator<ObjectClass> sourceClassIterator = sourceClasses.iterator();
773    while (sourceClassIterator.hasNext())
774    {
775      ObjectClass sourceClass = sourceClassIterator.next();
776      if (targetClasses.remove(sourceClass))
777      {
778        sourceClassIterator.remove();
779      }
780    }
781
782    if (!sourceClasses.isEmpty())
783    {
784      // Whatever is left must have been deleted.
785      AttributeType attrType = DirectoryServer.getObjectClassAttributeType();
786      AttributeBuilder builder = new AttributeBuilder(attrType);
787      for (ObjectClass oc : sourceClasses)
788      {
789        builder.add(oc.getNameOrOID());
790      }
791
792      modifications.add(new Modification(ModificationType.DELETE, builder
793          .toAttribute()));
794    }
795
796    if (! targetClasses.isEmpty())
797    {
798      // Whatever is left must have been added.
799      AttributeType attrType = DirectoryServer.getObjectClassAttributeType();
800      AttributeBuilder builder = new AttributeBuilder(attrType);
801      for (ObjectClass oc : targetClasses)
802      {
803        builder.add(oc.getNameOrOID());
804      }
805
806      modifications.add(new Modification(ModificationType.ADD, builder
807          .toAttribute()));
808    }
809
810
811    // Look at the user attributes for the entries.
812    LinkedHashSet<AttributeType> sourceTypes = new LinkedHashSet<>(sourceEntry.getUserAttributes().keySet());
813    Iterator<AttributeType> sourceTypeIterator = sourceTypes.iterator();
814    while (sourceTypeIterator.hasNext())
815    {
816      AttributeType   type        = sourceTypeIterator.next();
817      List<Attribute> sourceAttrs = sourceEntry.getUserAttribute(type);
818      List<Attribute> targetAttrs = targetEntry.getUserAttribute(type);
819      sourceEntry.removeAttribute(type);
820
821      if (targetAttrs == null)
822      {
823        // The target entry doesn't have this attribute type, so it must have
824        // been deleted.  In order to make the delete reversible, delete each
825        // value individually.
826        for (Attribute a : sourceAttrs)
827        {
828          modifications.add(new Modification(ModificationType.DELETE, a));
829        }
830      }
831      else
832      {
833        // Check the attributes for differences.  We'll ignore differences in
834        // the order of the values since that isn't significant.
835        targetEntry.removeAttribute(type);
836
837        for (Attribute sourceAttr : sourceAttrs)
838        {
839          Attribute targetAttr = null;
840          Iterator<Attribute> attrIterator = targetAttrs.iterator();
841          while (attrIterator.hasNext())
842          {
843            Attribute a = attrIterator.next();
844            if (a.optionsEqual(sourceAttr.getOptions()))
845            {
846              targetAttr = a;
847              attrIterator.remove();
848              break;
849            }
850          }
851
852          if (targetAttr == null)
853          {
854            // The attribute doesn't exist in the target list, so it has been
855            // deleted.
856            modifications.add(new Modification(ModificationType.DELETE,
857                                               sourceAttr));
858          }
859          else
860          {
861            // Compare the values.
862            AttributeBuilder addedValuesBuilder = new AttributeBuilder(targetAttr);
863            addedValuesBuilder.removeAll(sourceAttr);
864            Attribute addedValues = addedValuesBuilder.toAttribute();
865            if (!addedValues.isEmpty())
866            {
867              modifications.add(new Modification(ModificationType.ADD, addedValues));
868            }
869
870            AttributeBuilder deletedValuesBuilder = new AttributeBuilder(sourceAttr);
871            deletedValuesBuilder.removeAll(targetAttr);
872            Attribute deletedValues = deletedValuesBuilder.toAttribute();
873            if (!deletedValues.isEmpty())
874            {
875              modifications.add(new Modification(ModificationType.DELETE, deletedValues));
876            }
877          }
878        }
879
880
881        // Any remaining target attributes have been added.
882        for (Attribute targetAttr: targetAttrs)
883        {
884          modifications.add(new Modification(ModificationType.ADD, targetAttr));
885        }
886      }
887    }
888
889    // Any remaining target attribute types have been added.
890    for (AttributeType type : targetEntry.getUserAttributes().keySet())
891    {
892      List<Attribute> targetAttrs = targetEntry.getUserAttribute(type);
893      for (Attribute a : targetAttrs)
894      {
895        modifications.add(new Modification(ModificationType.ADD, a));
896      }
897    }
898
899    // Remove ignored attributes
900    if (! ignoreAttrs.isEmpty())
901    {
902      ListIterator<Modification> modIter = modifications.listIterator();
903      while (modIter.hasNext())
904      {
905        String name = modIter.next().getAttribute().getName().toLowerCase();
906        if (ignoreAttrs.contains(name))
907        {
908            modIter.remove();
909        }
910      }
911    }
912
913    // Write the modification change record.
914    if (modifications.isEmpty())
915    {
916      return false;
917    }
918
919    if (singleValueChanges)
920    {
921      for (Modification m : modifications)
922      {
923        Attribute a = m.getAttribute();
924        if (a.isEmpty())
925        {
926          writer.writeModifyChangeRecord(sourceEntry.getName(), newLinkedList(m));
927        }
928        else
929        {
930          LinkedList<Modification> attrMods = new LinkedList<>();
931          for (ByteString v : a)
932          {
933            AttributeBuilder builder = new AttributeBuilder(a, true);
934            builder.add(v);
935            Attribute attr = builder.toAttribute();
936
937            attrMods.clear();
938            attrMods.add(new Modification(m.getModificationType(), attr));
939            writer.writeModifyChangeRecord(sourceEntry.getName(), attrMods);
940          }
941        }
942      }
943    }
944    else
945    {
946      writer.writeModifyChangeRecord(sourceEntry.getName(), modifications);
947    }
948
949    return true;
950  }
951}