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