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-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.replication.plugin; 028 029import static org.opends.messages.ReplicationMessages.*; 030import static org.opends.server.replication.plugin.HistAttrModificationKey.*; 031 032import java.util.*; 033 034import org.forgerock.i18n.slf4j.LocalizedLogger; 035import org.forgerock.opendj.ldap.ByteString; 036import org.forgerock.opendj.ldap.ModificationType; 037import org.opends.server.core.DirectoryServer; 038import org.opends.server.replication.common.CSN; 039import org.opends.server.replication.protocol.OperationContext; 040import org.opends.server.types.*; 041import org.opends.server.types.operation.PreOperationAddOperation; 042import org.opends.server.types.operation.PreOperationModifyDNOperation; 043import org.opends.server.types.operation.PreOperationModifyOperation; 044import org.opends.server.util.TimeThread; 045 046/** 047 * This class is used to store historical information that is used to resolve modify conflicts 048 * <p> 049 * It is assumed that the common case is not to have conflict and therefore is optimized (in order 050 * of importance) for: 051 * <ol> 052 * <li>detecting potential conflict</li> 053 * <li>fast update of historical information for non-conflicting change</li> 054 * <li>fast and efficient purge</li> 055 * <li>compact</li> 056 * <li>solve conflict. This should also be as fast as possible but not at the cost of any of the 057 * other previous objectives</li> 058 * </ol> 059 * One Historical object is created for each entry in the entry cache each Historical Object 060 * contains a list of attribute historical information 061 */ 062public class EntryHistorical 063{ 064 /** Name of the attribute used to store historical information. */ 065 public static final String HISTORICAL_ATTRIBUTE_NAME = "ds-sync-hist"; 066 /** 067 * Name used to store attachment of historical information in the 068 * operation. This attachment allows to use in several different places 069 * the historical while reading/writing ONCE it from/to the entry. 070 */ 071 public static final String HISTORICAL = "ds-synch-historical"; 072 /** Name of the entryuuid attribute. */ 073 public static final String ENTRYUUID_ATTRIBUTE_NAME = "entryuuid"; 074 075 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 076 077 /** 078 * The delay to purge the historical information. 079 * <p> 080 * This delay indicates the time the domain keeps the historical information 081 * necessary to solve conflicts. When a change stored in the historical part 082 * of the user entry has a date (from its replication CSN) older than this 083 * delay, it is candidate to be purged. The purge is triggered on 2 events: 084 * modify of the entry, dedicated purge task. The purge is done when the 085 * historical is encoded. 086 */ 087 private long purgeDelayInMillisec = -1; 088 089 /** 090 * The oldest CSN stored in this entry historical attribute. 091 * null when this historical object has been created from 092 * an entry that has no historical attribute and after the last 093 * historical has been purged. 094 */ 095 private CSN oldestCSN; 096 097 /** 098 * For stats/monitoring purpose, the number of historical values 099 * purged the last time a purge has been applied on this entry historical. 100 */ 101 private int lastPurgedValuesCount; 102 103 /** 104 * The in-memory historical information is made of. 105 * 106 * EntryHistorical ::= ADDDate MODDNDate attributesInfo 107 * ADDDate ::= CSN // the date the entry was added 108 * MODDNDate ::= CSN // the date the entry was last renamed 109 * 110 * attributesInfo ::= (AttrInfoWithOptions)* 111 * one AttrInfoWithOptions by attributeType 112 * 113 * AttrInfoWithOptions ::= (AttributeInfo)* 114 * one AttributeInfo by attributeType and option 115 * 116 * AttributeInfo ::= AttrInfoSingle | AttrInfoMultiple 117 * 118 * AttrInfoSingle ::= AddTime DeleteTime ValueInfo 119 * 120 * AttrInfoMultiple ::= AddTime DeleteTime ValuesInfo 121 * 122 * ValuesInfo ::= (AttrValueHistorical)* 123 * AttrValueHistorical is the historical of the 124 * the modification of one value 125 * 126 * AddTime ::= CSN // last time the attribute was added to the entry 127 * DeleteTime ::= CSN // last time the attribute was deleted from the entry 128 * 129 * AttrValueHistorical ::= AttributeValue valueDeleteTime valueUpdateTime 130 * valueDeleteTime ::= CSN 131 * valueUpdateTime ::= CSN 132 * 133 * - a list indexed on AttributeType of AttrInfoWithOptions : 134 * each value is the historical for this attribute 135 * an AttrInfoWithOptions is a set indexed on the optionValue(string) of 136 * AttributeInfo 137 */ 138 139 /** The date when the entry was added. */ 140 private CSN entryADDDate; 141 142 /** The date when the entry was last renamed. */ 143 private CSN entryMODDNDate; 144 145 /** 146 * Contains Historical information for each attribute sorted by attribute 147 * type. key:AttributeType value:AttrInfoWithOptions 148 */ 149 private final HashMap<AttributeType,AttrHistoricalWithOptions> attributesHistorical = new HashMap<>(); 150 151 /** {@inheritDoc} */ 152 @Override 153 public String toString() 154 { 155 StringBuilder builder = new StringBuilder(); 156 builder.append(encodeAndPurge()); 157 return builder.toString(); 158 } 159 160 /** 161 * Process an operation. 162 * This method is responsible for detecting and resolving conflict for 163 * modifyOperation. This is done by using the historical information. 164 * 165 * @param modifyOperation the operation to be processed 166 * @param modifiedEntry the entry that is being modified (before modification) 167 * @return true if the replayed operation was in conflict 168 */ 169 public boolean replayOperation(PreOperationModifyOperation modifyOperation, Entry modifiedEntry) 170 { 171 boolean bConflict = false; 172 List<Modification> mods = modifyOperation.getModifications(); 173 CSN modOpCSN = OperationContext.getCSN(modifyOperation); 174 175 for (Iterator<Modification> it = mods.iterator(); it.hasNext(); ) 176 { 177 Modification m = it.next(); 178 179 // Read or create the attr historical for the attribute type and option 180 // contained in the mod 181 AttrHistorical attrHist = getOrCreateAttrHistorical(m); 182 if (attrHist.replayOperation(it, modOpCSN, modifiedEntry, m)) 183 { 184 bConflict = true; 185 } 186 } 187 188 return bConflict; 189 } 190 191 /** 192 * Update the historical information for the provided operation. 193 * <p> 194 * Steps: 195 * <ul> 196 * <li>compute the historical attribute</li> 197 * <li>update the mods in the provided operation by adding the update of the 198 * historical attribute</li> 199 * <li>update the modifiedEntry, already computed by core since we are in the 200 * preOperation plugin, that is called just before committing into the DB. 201 * </li> 202 * </ul> 203 * </p> 204 * 205 * @param modifyOperation 206 * the modification. 207 */ 208 public void setHistoricalAttrToOperation(PreOperationModifyOperation modifyOperation) 209 { 210 List<Modification> mods = modifyOperation.getModifications(); 211 Entry modifiedEntry = modifyOperation.getModifiedEntry(); 212 CSN csn = OperationContext.getCSN(modifyOperation); 213 214 /* 215 * If this is a local operation we need : 216 * - first to update the historical information, 217 * - then update the entry with the historical information 218 * If this is a replicated operation the historical information has 219 * already been set in the resolveConflict phase and we only need 220 * to update the entry 221 */ 222 if (!modifyOperation.isSynchronizationOperation()) 223 { 224 for (Modification mod : mods) 225 { 226 // Get the current historical for this attributeType/options 227 // (eventually read from the provided modification) 228 AttrHistorical attrHist = getOrCreateAttrHistorical(mod); 229 if (attrHist != null) 230 { 231 attrHist.processLocalOrNonConflictModification(csn, mod); 232 } 233 } 234 } 235 236 // Now do the 2 updates required by the core to be consistent: 237 // 238 // - add the modification of the ds-sync-hist attribute, 239 // to the current modifications of the MOD operation 240 Attribute attr = encodeAndPurge(); 241 mods.add(new Modification(ModificationType.REPLACE, attr)); 242 // - update the already modified entry 243 modifiedEntry.replaceAttribute(attr); 244 } 245 246 /** 247 * For a MODDN operation, add new or update existing historical information. 248 * <p> 249 * This method is NOT static because it relies on this Historical object created in the 250 * HandleConflictResolution phase. 251 * 252 * @param modifyDNOperation 253 * the modification for which the historical information should be created. 254 */ 255 public void setHistoricalAttrToOperation(PreOperationModifyDNOperation modifyDNOperation) 256 { 257 // Update this historical information with the operation CSN. 258 this.entryMODDNDate = OperationContext.getCSN(modifyDNOperation); 259 260 // Update the operations mods and the modified entry so that the 261 // historical information gets stored in the DB and indexed accordingly. 262 Entry modifiedEntry = modifyDNOperation.getUpdatedEntry(); 263 List<Modification> mods = modifyDNOperation.getModifications(); 264 265 Attribute attr = encodeAndPurge(); 266 267 // Now do the 2 updates required by the core to be consistent: 268 // 269 // - add the modification of the ds-sync-hist attribute, 270 // to the current modifications of the operation 271 mods.add(new Modification(ModificationType.REPLACE, attr)); 272 // - update the already modified entry 273 modifiedEntry.removeAttribute(attr.getAttributeType()); 274 modifiedEntry.addAttribute(attr, null); 275 } 276 277 /** 278 * Generate an attribute containing the historical information 279 * from the replication context attached to the provided operation 280 * and set this attribute in the operation. 281 * 282 * For ADD, the historical is made of the CSN read from the 283 * synchronization context attached to the operation. 284 * 285 * Called for both local and synchronization ADD preOperation. 286 * 287 * This historical information will be used to generate fake operation 288 * in case a Directory Server can not find a Replication Server with 289 * all its changes at connection time. 290 * This should only happen if a Directory Server or a Replication Server 291 * crashes. 292 * 293 * This method is static because there is no Historical object creation 294 * required here or before(in the HandleConflictResolution phase) 295 * 296 * @param addOperation The Operation to which the historical attribute will be added. 297 */ 298 public static void setHistoricalAttrToOperation(PreOperationAddOperation addOperation) 299 { 300 AttributeType attrType = DirectoryServer.getAttributeType(HISTORICAL_ATTRIBUTE_NAME); 301 String attrValue = encodeHistorical(OperationContext.getCSN(addOperation), "add"); 302 List<Attribute> attrs = Attributes.createAsList(attrType, attrValue); 303 addOperation.setAttribute(attrType, attrs); 304 } 305 306 /** 307 * Builds an attributeValue for the supplied historical information and 308 * operation type . For ADD Operation : "dn:changeNumber:add", for MODDN 309 * Operation : "dn:changeNumber:moddn", etc. 310 * 311 * @param csn 312 * The date when the ADD Operation happened. 313 * @param operationType 314 * the operation type to encode 315 * @return The attribute value containing the historical information for the Operation type. 316 */ 317 private static String encodeHistorical(CSN csn, String operationType) 318 { 319 return "dn:" + csn + ":" + operationType; 320 } 321 322 /** 323 * Return an AttributeHistorical corresponding to the attribute type 324 * and options contained in the provided mod, 325 * The attributeHistorical is : 326 * - either read from this EntryHistorical object if one exist, 327 * - or created empty. 328 * Should never return null. 329 * 330 * @param mod the provided mod from which we'll use attributeType 331 * and options to retrieve/create the attribute historical 332 * @return the attribute historical retrieved or created empty. 333 */ 334 private AttrHistorical getOrCreateAttrHistorical(Modification mod) 335 { 336 // Read the provided mod 337 Attribute modAttr = mod.getAttribute(); 338 if (isHistoricalAttribute(modAttr)) 339 { 340 // Don't keep historical information for the attribute that is 341 // used to store the historical information. 342 return null; 343 } 344 Set<String> modOptions = modAttr.getOptions(); 345 AttributeType modAttrType = modAttr.getAttributeType(); 346 347 // Read from this entryHistorical, 348 // Create one empty if none was existing in this entryHistorical. 349 AttrHistoricalWithOptions attrHistWithOptions = attributesHistorical.get(modAttrType); 350 AttrHistorical attrHist; 351 if (attrHistWithOptions != null) 352 { 353 attrHist = attrHistWithOptions.get(modOptions); 354 } 355 else 356 { 357 attrHistWithOptions = new AttrHistoricalWithOptions(); 358 attributesHistorical.put(modAttrType, attrHistWithOptions); 359 attrHist = null; 360 } 361 362 if (attrHist == null) 363 { 364 attrHist = AttrHistorical.createAttributeHistorical(modAttrType); 365 attrHistWithOptions.put(modOptions, attrHist); 366 } 367 return attrHist; 368 } 369 370 /** 371 * For stats/monitoring purpose, returns the number of historical values 372 * purged the last time a purge has been applied on this entry historical. 373 * 374 * @return the purged values count. 375 */ 376 public int getLastPurgedValuesCount() 377 { 378 return this.lastPurgedValuesCount; 379 } 380 381 /** 382 * Encode this historical information object in an operational attribute and 383 * purge it from the values older than the purge delay. 384 * 385 * @return The historical information encoded in an operational attribute. 386 * @see HistoricalAttributeValue#HistoricalAttributeValue(String) the decode 387 * operation in HistoricalAttributeValue 388 */ 389 public Attribute encodeAndPurge() 390 { 391 long purgeDate = 0; 392 393 // Set the stats counter to 0 and compute the purgeDate to now minus 394 // the potentially set purge delay. 395 this.lastPurgedValuesCount = 0; 396 if (purgeDelayInMillisec>0) 397 { 398 purgeDate = TimeThread.getTime() - purgeDelayInMillisec; 399 } 400 401 AttributeType historicalAttrType = DirectoryServer.getAttributeType(HISTORICAL_ATTRIBUTE_NAME); 402 AttributeBuilder builder = new AttributeBuilder(historicalAttrType); 403 404 for (Map.Entry<AttributeType, AttrHistoricalWithOptions> entryWithOptions : 405 attributesHistorical.entrySet()) 406 { 407 // Encode an attribute type 408 AttributeType type = entryWithOptions.getKey(); 409 Map<Set<String>, AttrHistorical> attrWithOptions = 410 entryWithOptions.getValue().getAttributesInfo(); 411 412 for (Map.Entry<Set<String>, AttrHistorical> entry : attrWithOptions.entrySet()) 413 { 414 // Encode an (attribute type/option) 415 String optionsString = toOptionsString(entry.getKey()); 416 AttrHistorical attrHist = entry.getValue(); 417 418 CSN deleteTime = attrHist.getDeleteTime(); 419 /* generate the historical information for deleted attributes */ 420 boolean attrDel = deleteTime != null; 421 422 for (AttrValueHistorical attrValHist : attrHist.getValuesHistorical()) 423 { 424 final ByteString value = attrValHist.getAttributeValue(); 425 426 // Encode an attribute value 427 if (attrValHist.getValueDeleteTime() != null) 428 { 429 if (needsPurge(attrValHist.getValueDeleteTime(), purgeDate)) 430 { 431 // this hist must be purged now, so skip its encoding 432 continue; 433 } 434 String strValue = encode(DEL, type, optionsString, attrValHist.getValueDeleteTime(), value); 435 builder.add(strValue); 436 } 437 else if (attrValHist.getValueUpdateTime() != null) 438 { 439 if (needsPurge(attrValHist.getValueUpdateTime(), purgeDate)) 440 { 441 // this hist must be purged now, so skip its encoding 442 continue; 443 } 444 445 String strValue; 446 final CSN updateTime = attrValHist.getValueUpdateTime(); 447 // FIXME very suspicious use of == in the next if statement, 448 // unit tests do not like changing it 449 if (attrDel && updateTime == deleteTime && value != null) 450 { 451 strValue = encode(REPL, type, optionsString, updateTime, value); 452 attrDel = false; 453 } 454 else if (value != null) 455 { 456 strValue = encode(ADD, type, optionsString, updateTime, value); 457 } 458 else 459 { 460 // "add" without any value is suspicious. Tests never go there. 461 // Is this used to encode "add" with an empty string? 462 strValue = encode(ADD, type, optionsString, updateTime); 463 } 464 465 builder.add(strValue); 466 } 467 } 468 469 if (attrDel) 470 { 471 if (needsPurge(deleteTime, purgeDate)) 472 { 473 // this hist must be purged now, so skip its encoding 474 continue; 475 } 476 String strValue = encode(ATTRDEL, type, optionsString, deleteTime); 477 builder.add(strValue); 478 } 479 } 480 } 481 482 if (entryADDDate != null && !needsPurge(entryADDDate, purgeDate)) 483 { 484 // Encode the historical information for the ADD Operation. 485 // Stores the ADDDate when not older than the purge delay 486 builder.add(encodeHistorical(entryADDDate, "add")); 487 } 488 489 if (entryMODDNDate != null && !needsPurge(entryMODDNDate, purgeDate)) 490 { 491 // Encode the historical information for the MODDN Operation. 492 // Stores the MODDNDate when not older than the purge delay 493 builder.add(encodeHistorical(entryMODDNDate, "moddn")); 494 } 495 496 return builder.toAttribute(); 497 } 498 499 private String toOptionsString(Set<String> options) 500 { 501 if (options != null) 502 { 503 StringBuilder optionsBuilder = new StringBuilder(); 504 for (String s : options) 505 { 506 optionsBuilder.append(';').append(s); 507 } 508 return optionsBuilder.toString(); 509 } 510 return ""; 511 } 512 513 private boolean needsPurge(CSN csn, long purgeDate) 514 { 515 boolean needsPurge = purgeDelayInMillisec > 0 && csn.getTime() <= purgeDate; 516 if (needsPurge) 517 { 518 // this hist must be purged now, because older than the purge delay 519 this.lastPurgedValuesCount++; 520 } 521 return needsPurge; 522 } 523 524 private String encode(HistAttrModificationKey modKey, AttributeType type, 525 String optionsString, CSN changeTime) 526 { 527 return type.getNormalizedPrimaryName() + optionsString + ":" + changeTime + ":" + modKey; 528 } 529 530 private String encode(HistAttrModificationKey modKey, AttributeType type, 531 String optionsString, CSN changeTime, ByteString value) 532 { 533 return type.getNormalizedPrimaryName() + optionsString + ":" + changeTime + ":" + modKey + ":" + value; 534 } 535 536 /** 537 * Set the delay to purge the historical information. The purge is applied 538 * only when historical attribute is updated (write operations). 539 * 540 * @param purgeDelay the purge delay in ms 541 */ 542 public void setPurgeDelay(long purgeDelay) 543 { 544 this.purgeDelayInMillisec = purgeDelay; 545 } 546 547 /** 548 * Indicates if the Entry was renamed or added after the CSN that is given as 549 * a parameter. 550 * 551 * @param csn 552 * The CSN with which the ADD or Rename date must be compared. 553 * @return A boolean indicating if the Entry was renamed or added after the 554 * CSN that is given as a parameter. 555 */ 556 public boolean addedOrRenamedAfter(CSN csn) 557 { 558 return csn.isOlderThan(entryADDDate) || csn.isOlderThan(entryMODDNDate); 559 } 560 561 /** 562 * Returns the lastCSN when the entry DN was modified. 563 * 564 * @return The lastCSN when the entry DN was modified. 565 */ 566 public CSN getDNDate() 567 { 568 if (entryADDDate == null) 569 { 570 return entryMODDNDate; 571 } 572 if (entryMODDNDate == null) 573 { 574 return entryADDDate; 575 } 576 577 if (entryMODDNDate.isOlderThan(entryADDDate)) 578 { 579 return entryMODDNDate; 580 } 581 else 582 { 583 return entryADDDate; 584 } 585 } 586 587 /** 588 * Construct an Historical object from the provided entry by reading the historical attribute. 589 * Return an empty object when the entry does not contain any historical attribute. 590 * 591 * @param entry The entry which historical information must be loaded 592 * @return The constructed Historical information object 593 */ 594 public static EntryHistorical newInstanceFromEntry(Entry entry) 595 { 596 // Read the DB historical attribute from the entry 597 List<Attribute> histAttrWithOptionsFromEntry = getHistoricalAttr(entry); 598 599 // Now we'll build the Historical object we want to construct 600 final EntryHistorical newHistorical = new EntryHistorical(); 601 if (histAttrWithOptionsFromEntry == null) 602 { 603 // No historical attribute in the entry, return empty object 604 return newHistorical; 605 } 606 607 try 608 { 609 AttributeType lastAttrType = null; 610 Set<String> lastOptions = new HashSet<>(); 611 AttrHistorical attrInfo = null; 612 AttrHistoricalWithOptions attrInfoWithOptions = null; 613 614 // For each value of the historical attr read (mod. on a user attribute) 615 // build an AttrInfo sub-object 616 617 // Traverse the Attributes (when several options for the hist attr) 618 // of the historical attribute read from the entry 619 for (Attribute histAttrFromEntry : histAttrWithOptionsFromEntry) 620 { 621 // For each Attribute (option), traverse the values 622 for (ByteString histAttrValueFromEntry : histAttrFromEntry) 623 { 624 // From each value of the hist attr, create an object 625 final HistoricalAttributeValue histVal = new HistoricalAttributeValue(histAttrValueFromEntry.toString()); 626 final CSN csn = histVal.getCSN(); 627 628 // update the oldest CSN stored in the new entry historical 629 newHistorical.updateOldestCSN(csn); 630 631 if (histVal.isADDOperation()) 632 { 633 newHistorical.entryADDDate = csn; 634 } 635 else if (histVal.isMODDNOperation()) 636 { 637 newHistorical.entryMODDNDate = csn; 638 } 639 else 640 { 641 AttributeType attrType = histVal.getAttrType(); 642 if (attrType == null) 643 { 644 /* 645 * This attribute is unknown from the schema 646 * Just skip it, the modification will be processed but no 647 * historical information is going to be kept. 648 * Log information for the repair tool. 649 */ 650 logger.error(ERR_UNKNOWN_ATTRIBUTE_IN_HISTORICAL, entry.getName(), histVal.getAttrString()); 651 continue; 652 } 653 654 /* if attribute type does not match we create new 655 * AttrInfoWithOptions and AttrInfo 656 * we also add old AttrInfoWithOptions into histObj.attributesInfo 657 * if attribute type match but options does not match we create new 658 * AttrInfo that we add to AttrInfoWithOptions 659 * if both match we keep everything 660 */ 661 Set<String> options = histVal.getOptions(); 662 if (attrType != lastAttrType) 663 { 664 attrInfo = AttrHistorical.createAttributeHistorical(attrType); 665 666 // Create attrInfoWithOptions and store inside the attrInfo 667 attrInfoWithOptions = new AttrHistoricalWithOptions(); 668 attrInfoWithOptions.put(options, attrInfo); 669 670 // Store this attrInfoWithOptions in the newHistorical object 671 newHistorical.attributesHistorical.put(attrType, attrInfoWithOptions); 672 673 lastAttrType = attrType; 674 lastOptions = options; 675 } 676 else if (!options.equals(lastOptions)) 677 { 678 attrInfo = AttrHistorical.createAttributeHistorical(attrType); 679 attrInfoWithOptions.put(options, attrInfo); 680 lastOptions = options; 681 } 682 683 attrInfo.assign(histVal.getHistKey(), histVal.getAttributeValue(), csn); 684 } 685 } 686 } 687 } catch (Exception e) 688 { 689 // Any exception happening here means that the coding of the historical 690 // information was wrong. 691 // Log an error and continue with an empty historical. 692 logger.error(ERR_BAD_HISTORICAL, entry.getName()); 693 } 694 695 /* set the reference to the historical information in the entry */ 696 return newHistorical; 697 } 698 699 /** 700 * Use this historical information to generate fake operations that would 701 * result in this historical information. 702 * TODO : This is only implemented for MODIFY, MODRDN and ADD 703 * need to complete with DELETE. 704 * @param entry The Entry to use to generate the FakeOperation Iterable. 705 * 706 * @return an Iterable of FakeOperation that would result in this historical information. 707 */ 708 public static Iterable<FakeOperation> generateFakeOperations(Entry entry) 709 { 710 TreeMap<CSN, FakeOperation> operations = new TreeMap<>(); 711 List<Attribute> attrs = getHistoricalAttr(entry); 712 if (attrs != null) 713 { 714 for (Attribute attr : attrs) 715 { 716 for (ByteString val : attr) 717 { 718 HistoricalAttributeValue histVal = new HistoricalAttributeValue(val.toString()); 719 if (histVal.isADDOperation()) 720 { 721 // Found some historical information indicating that this entry was just added. 722 // Create the corresponding ADD operation. 723 operations.put(histVal.getCSN(), new FakeAddOperation(histVal.getCSN(), entry)); 724 } 725 else if (histVal.isMODDNOperation()) 726 { 727 // Found some historical information indicating that this entry was just renamed. 728 // Create the corresponding ADD operation. 729 operations.put(histVal.getCSN(), new FakeModdnOperation(histVal.getCSN(), entry)); 730 } 731 else 732 { 733 // Found some historical information for modify operation. 734 // Generate the corresponding ModifyOperation or update 735 // the already generated Operation if it can be found. 736 CSN csn = histVal.getCSN(); 737 Modification mod = histVal.generateMod(); 738 FakeOperation fakeOperation = operations.get(csn); 739 740 if (fakeOperation instanceof FakeModifyOperation) 741 { 742 FakeModifyOperation modifyFakeOperation = (FakeModifyOperation) fakeOperation; 743 modifyFakeOperation.addModification(mod); 744 } 745 else 746 { 747 String uuidString = getEntryUUID(entry); 748 FakeModifyOperation modifyFakeOperation = 749 new FakeModifyOperation(entry.getName(), csn, uuidString); 750 modifyFakeOperation.addModification(mod); 751 operations.put(histVal.getCSN(), modifyFakeOperation); 752 } 753 } 754 } 755 } 756 } 757 return operations.values(); 758 } 759 760 /** 761 * Get the attribute used to store the historical information from the provided Entry. 762 * 763 * @param entry The entry containing the historical information. 764 * @return The Attribute used to store the historical information. 765 * Several values on the list if several options for this attribute. 766 * Null if not present. 767 */ 768 public static List<Attribute> getHistoricalAttr(Entry entry) 769 { 770 return entry.getAttribute(HISTORICAL_ATTRIBUTE_NAME); 771 } 772 773 /** 774 * Get the entry unique Id in String form. 775 * 776 * @param entry The entry for which the unique id should be returned. 777 * @return The Unique Id of the entry, or a fake one if none is found. 778 */ 779 public static String getEntryUUID(Entry entry) 780 { 781 AttributeType attrType = DirectoryServer.getAttributeType(ENTRYUUID_ATTRIBUTE_NAME); 782 List<Attribute> uuidAttrs = entry.getOperationalAttribute(attrType); 783 return extractEntryUUID(uuidAttrs, entry.getName()); 784 } 785 786 /** 787 * Get the Entry Unique Id from an add operation. 788 * This must be called after the entry uuid pre-op plugin (i.e no 789 * sooner than the replication provider pre-op) 790 * 791 * @param op The operation 792 * @return The Entry Unique Id String form. 793 */ 794 public static String getEntryUUID(PreOperationAddOperation op) 795 { 796 AttributeType attrType = DirectoryServer.getAttributeType(ENTRYUUID_ATTRIBUTE_NAME); 797 List<Attribute> uuidAttrs = op.getOperationalAttributes().get(attrType); 798 return extractEntryUUID(uuidAttrs, op.getEntryDN()); 799 } 800 801 /** 802 * Check if a given attribute is an attribute used to store historical 803 * information. 804 * 805 * @param attr The attribute that needs to be checked. 806 * 807 * @return a boolean indicating if the given attribute is 808 * used to store historical information. 809 */ 810 public static boolean isHistoricalAttribute(Attribute attr) 811 { 812 AttributeType attrType = attr.getAttributeType(); 813 return HISTORICAL_ATTRIBUTE_NAME.equals(attrType.getNameOrOID()); 814 } 815 816 /** 817 * Potentially update the oldest CSN stored in this entry historical 818 * with the provided CSN when its older than the current oldest. 819 * 820 * @param csn the provided CSN. 821 */ 822 private void updateOldestCSN(CSN csn) 823 { 824 if (csn != null 825 && (this.oldestCSN == null || csn.isOlderThan(this.oldestCSN))) 826 { 827 this.oldestCSN = csn; 828 } 829 } 830 831 /** 832 * Returns the oldest CSN stored in this entry historical attribute. 833 * 834 * @return the oldest CSN stored in this entry historical attribute. 835 * Returns null when this historical object has been created from 836 * an entry that has no historical attribute and after the last 837 * historical has been purged. 838 */ 839 public CSN getOldestCSN() 840 { 841 return this.oldestCSN; 842 } 843 844 /** 845 * Extracts the entryUUID attribute value from the provided list of 846 * attributes. If the attribute is not present one is generated from the DN 847 * using the same algorithm as the entryUUID virtual attribute provider. 848 */ 849 private static String extractEntryUUID(List<Attribute> entryUUIDAttributes, DN entryDN) 850 { 851 if (entryUUIDAttributes != null) 852 { 853 Attribute uuidAttr = entryUUIDAttributes.get(0); 854 if (!uuidAttr.isEmpty()) 855 { 856 return uuidAttr.iterator().next().toString(); 857 } 858 } 859 860 // Generate a fake entryUUID: see OPENDJ-181. In rare pathological cases 861 // an entryUUID attribute may not be present and this causes severe side effects 862 // for replication which requires the attribute to always be present 863 if (logger.isTraceEnabled()) 864 { 865 logger.trace( 866 "Replication requires an entryUUID attribute in order " 867 + "to perform conflict resolution, but none was " 868 + "found in entry \"%s\": generating virtual entryUUID instead", 869 entryDN); 870 } 871 872 return UUID.nameUUIDFromBytes(entryDN.toNormalizedByteString().toByteArray()).toString(); 873 } 874}