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