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