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}