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