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