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