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}