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