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