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  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
065
066  /** Name of the attribute used to store historical information. */
067  public static final String HISTORICAL_ATTRIBUTE_NAME = "ds-sync-hist";
068  /**
069   * Name used to store attachment of historical information in the
070   * operation. This attachment allows to use in several different places
071   * the historical while reading/writing ONCE it from/to the entry.
072   */
073  public static final String HISTORICAL = "ds-synch-historical";
074  /** Name of the entryuuid attribute. */
075  public static final String ENTRYUUID_ATTRIBUTE_NAME = "entryuuid";
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  /** The date when the entry was added. */
104  private CSN entryADDDate;
105  /** The date when the entry was last renamed. */
106  private CSN entryMODDNDate;
107
108  /** Contains Historical information for each attribute description. */
109  private final Map<AttributeDescription, AttrHistorical> attributesHistorical = new HashMap<>();
110
111  @Override
112  public String toString()
113  {
114    StringBuilder builder = new StringBuilder();
115    builder.append(encodeAndPurge());
116    return builder.toString();
117  }
118
119  /**
120   * Process an operation.
121   * This method is responsible for detecting and resolving conflict for
122   * modifyOperation. This is done by using the historical information.
123   *
124   * @param modifyOperation the operation to be processed
125   * @param modifiedEntry the entry that is being modified (before modification)
126   * @return true if the replayed operation was in conflict
127   */
128  public boolean replayOperation(PreOperationModifyOperation modifyOperation, Entry modifiedEntry)
129  {
130    boolean bConflict = false;
131    List<Modification> mods = modifyOperation.getModifications();
132    CSN modOpCSN = OperationContext.getCSN(modifyOperation);
133
134    for (Iterator<Modification> it = mods.iterator(); it.hasNext(); )
135    {
136      Modification m = it.next();
137
138      // Read or create the attr historical for the attribute type and option
139      // contained in the mod
140      AttrHistorical attrHist = getOrCreateAttrHistorical(m);
141      if (attrHist.replayOperation(it, modOpCSN, modifiedEntry, m))
142      {
143        bConflict = true;
144      }
145    }
146
147    return bConflict;
148  }
149
150  /**
151   * Update the historical information for the provided operation.
152   * <p>
153   * Steps:
154   * <ul>
155   * <li>compute the historical attribute</li>
156   * <li>update the mods in the provided operation by adding the update of the
157   * historical attribute</li>
158   * <li>update the modifiedEntry, already computed by core since we are in the
159   * preOperation plugin, that is called just before committing into the DB.
160   * </li>
161   * </ul>
162   * </p>
163   *
164   * @param modifyOperation
165   *          the modification.
166   */
167  public void setHistoricalAttrToOperation(PreOperationModifyOperation modifyOperation)
168  {
169    List<Modification> mods = modifyOperation.getModifications();
170    Entry modifiedEntry = modifyOperation.getModifiedEntry();
171    CSN csn = OperationContext.getCSN(modifyOperation);
172
173    /*
174     * If this is a local operation we need :
175     * - first to update the historical information,
176     * - then update the entry with the historical information
177     * If this is a replicated operation the historical information has
178     * already been set in the resolveConflict phase and we only need
179     * to update the entry
180     */
181    if (!modifyOperation.isSynchronizationOperation())
182    {
183      for (Modification mod : mods)
184      {
185        // Get the current historical for this attributeType/options
186        // (eventually read from the provided modification)
187        AttrHistorical attrHist = getOrCreateAttrHistorical(mod);
188        if (attrHist != null)
189        {
190          attrHist.processLocalOrNonConflictModification(csn, mod);
191        }
192      }
193    }
194
195    // Now do the 2 updates required by the core to be consistent:
196    //
197    // - add the modification of the ds-sync-hist attribute,
198    // to the current modifications of the MOD operation
199    Attribute attr = encodeAndPurge();
200    mods.add(new Modification(ModificationType.REPLACE, attr));
201    // - update the already modified entry
202    modifiedEntry.replaceAttribute(attr);
203  }
204
205  /**
206   * For a MODDN operation, add new or update existing historical information.
207   * <p>
208   * This method is NOT static because it relies on this Historical object created in the
209   * HandleConflictResolution phase.
210   *
211   * @param modifyDNOperation
212   *          the modification for which the historical information should be created.
213   */
214  public void setHistoricalAttrToOperation(PreOperationModifyDNOperation modifyDNOperation)
215  {
216    // Update this historical information with the operation CSN.
217    this.entryMODDNDate = OperationContext.getCSN(modifyDNOperation);
218
219    // Update the operations mods and the modified entry so that the
220    // historical information gets stored in the DB and indexed accordingly.
221    Entry modifiedEntry = modifyDNOperation.getUpdatedEntry();
222    List<Modification> mods = modifyDNOperation.getModifications();
223
224    Attribute attr = encodeAndPurge();
225
226    // Now do the 2 updates required by the core to be consistent:
227    //
228    // - add the modification of the ds-sync-hist attribute,
229    // to the current modifications of the operation
230    mods.add(new Modification(ModificationType.REPLACE, attr));
231    // - update the already modified entry
232    modifiedEntry.removeAttribute(attr.getAttributeType());
233    modifiedEntry.addAttribute(attr, null);
234  }
235
236  /**
237   * Generate an attribute containing the historical information
238   * from the replication context attached to the provided operation
239   * and set this attribute in the operation.
240   *
241   *   For ADD, the historical is made of the CSN read from the
242   *   synchronization context attached to the operation.
243   *
244   *   Called for both local and synchronization ADD preOperation.
245   *
246   *   This historical information will be used to generate fake operation
247   *   in case a Directory Server can not find a Replication Server with
248   *   all its changes at connection time.
249   *   This should only happen if a Directory Server or a Replication Server
250   *   crashes.
251   *
252   *   This method is static because there is no Historical object creation
253   *   required here or before(in the HandleConflictResolution phase)
254   *
255   * @param addOperation The Operation to which the historical attribute will be added.
256   */
257  public static void setHistoricalAttrToOperation(PreOperationAddOperation addOperation)
258  {
259    AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(HISTORICAL_ATTRIBUTE_NAME);
260    String attrValue = encodeHistorical(OperationContext.getCSN(addOperation), "add");
261    List<Attribute> attrs = Attributes.createAsList(attrType, attrValue);
262    addOperation.setAttribute(attrType, attrs);
263  }
264
265  /**
266   * Builds an attributeValue for the supplied historical information and
267   * operation type . For ADD Operation : "dn:changeNumber:add", for MODDN
268   * Operation : "dn:changeNumber:moddn", etc.
269   *
270   * @param csn
271   *          The date when the ADD Operation happened.
272   * @param operationType
273   *          the operation type to encode
274   * @return The attribute value containing the historical information for the Operation type.
275   */
276  private static String encodeHistorical(CSN csn, String operationType)
277  {
278    return "dn:" + csn + ":" + operationType;
279  }
280
281  /**
282   * Return an AttributeHistorical corresponding to the attribute type
283   * and options contained in the provided mod,
284   * The attributeHistorical is :
285   * - either read from this EntryHistorical object if one exist,
286   * - or created empty.
287   * Should never return null.
288   *
289   * @param  mod the provided mod from which we'll use attributeType
290   *             and options to retrieve/create the attribute historical
291   * @return the attribute historical retrieved or created empty.
292   */
293  private AttrHistorical getOrCreateAttrHistorical(Modification mod)
294  {
295    // Read the provided mod
296    Attribute modAttr = mod.getAttribute();
297    if (isHistoricalAttribute(modAttr))
298    {
299      // Don't keep historical information for the attribute that is
300      // used to store the historical information.
301      return null;
302    }
303
304    // Read from this entryHistorical,
305    // Create one empty if none was existing in this entryHistorical.
306    AttributeDescription attrDesc = AttributeDescription.create(modAttr);
307    AttrHistorical attrHist = attributesHistorical.get(attrDesc);
308    if (attrHist == null)
309    {
310      attrHist = AttrHistorical.createAttributeHistorical(modAttr.getAttributeType());
311      attributesHistorical.put(attrDesc, attrHist);
312    }
313    return attrHist;
314  }
315
316  /**
317   * For stats/monitoring purpose, returns the number of historical values
318   * purged the last time a purge has been applied on this entry historical.
319   *
320   * @return the purged values count.
321   */
322  public int getLastPurgedValuesCount()
323  {
324    return this.lastPurgedValuesCount;
325  }
326
327  /**
328   * Encode this historical information object in an operational attribute and
329   * purge it from the values older than the purge delay.
330   *
331   * @return The historical information encoded in an operational attribute.
332   * @see HistoricalAttributeValue#HistoricalAttributeValue(String) the decode
333   *      operation in HistoricalAttributeValue
334   */
335  public Attribute encodeAndPurge()
336  {
337    long purgeDate = 0;
338
339    // Set the stats counter to 0 and compute the purgeDate to now minus
340    // the potentially set purge delay.
341    this.lastPurgedValuesCount = 0;
342    if (purgeDelayInMillisec>0)
343    {
344      purgeDate = TimeThread.getTime() - purgeDelayInMillisec;
345    }
346
347    AttributeType historicalAttrType = DirectoryServer.getAttributeTypeOrNull(HISTORICAL_ATTRIBUTE_NAME);
348    AttributeBuilder builder = new AttributeBuilder(historicalAttrType);
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 == null)
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    List<Attribute> attrs = getHistoricalAttr(entry);
611    if (attrs != null)
612    {
613      for (Attribute attr : attrs)
614      {
615        for (ByteString val : attr)
616        {
617          HistoricalAttributeValue histVal = new HistoricalAttributeValue(val.toString());
618          if (histVal.isADDOperation())
619          {
620            // Found some historical information indicating that this entry was just added.
621            // Create the corresponding ADD operation.
622            operations.put(histVal.getCSN(), new FakeAddOperation(histVal.getCSN(), entry));
623          }
624          else if (histVal.isMODDNOperation())
625          {
626            // Found some historical information indicating that this entry was just renamed.
627            // Create the corresponding ADD operation.
628            operations.put(histVal.getCSN(), new FakeModdnOperation(histVal.getCSN(), entry));
629          }
630          else
631          {
632            // Found some historical information for modify operation.
633            // Generate the corresponding ModifyOperation or update
634            // the already generated Operation if it can be found.
635            CSN csn = histVal.getCSN();
636            Modification mod = histVal.generateMod();
637            FakeOperation fakeOperation = operations.get(csn);
638
639            if (fakeOperation instanceof FakeModifyOperation)
640            {
641              FakeModifyOperation modifyFakeOperation = (FakeModifyOperation) fakeOperation;
642              modifyFakeOperation.addModification(mod);
643            }
644            else
645            {
646              String uuidString = getEntryUUID(entry);
647              FakeModifyOperation modifyFakeOperation =
648                  new FakeModifyOperation(entry.getName(), csn, uuidString);
649              modifyFakeOperation.addModification(mod);
650              operations.put(histVal.getCSN(), modifyFakeOperation);
651            }
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  public 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    AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(ENTRYUUID_ATTRIBUTE_NAME);
681    List<Attribute> uuidAttrs = entry.getOperationalAttribute(attrType);
682    return extractEntryUUID(uuidAttrs, entry.getName());
683  }
684
685  /**
686   * Get the Entry Unique Id from an add operation.
687   * This must be called after the entry uuid pre-op plugin (i.e no
688   * sooner than the replication provider pre-op)
689   *
690   * @param op The operation
691   * @return The Entry Unique Id String form.
692   */
693  public static String getEntryUUID(PreOperationAddOperation op)
694  {
695    AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(ENTRYUUID_ATTRIBUTE_NAME);
696    List<Attribute> uuidAttrs = op.getOperationalAttributes().get(attrType);
697    return extractEntryUUID(uuidAttrs, op.getEntryDN());
698  }
699
700  /**
701   * Check if a given attribute is an attribute used to store historical
702   * information.
703   *
704   * @param   attr The attribute that needs to be checked.
705   *
706   * @return  a boolean indicating if the given attribute is
707   *          used to store historical information.
708   */
709  public static boolean isHistoricalAttribute(Attribute attr)
710  {
711    AttributeType attrType = attr.getAttributeType();
712    return HISTORICAL_ATTRIBUTE_NAME.equals(attrType.getNameOrOID());
713  }
714
715  /**
716   * Potentially update the oldest CSN stored in this entry historical
717   * with the provided CSN when its older than the current oldest.
718   *
719   * @param csn the provided CSN.
720   */
721  private void updateOldestCSN(CSN csn)
722  {
723    if (csn != null
724        && (this.oldestCSN == null || csn.isOlderThan(this.oldestCSN)))
725    {
726      this.oldestCSN = csn;
727    }
728  }
729
730  /**
731   * Returns the oldest CSN stored in this entry historical attribute.
732   *
733   * @return the oldest CSN stored in this entry historical attribute.
734   *         Returns null when this historical object has been created from
735   *         an entry that has no historical attribute and after the last
736   *         historical has been purged.
737   */
738  public CSN getOldestCSN()
739  {
740    return this.oldestCSN;
741  }
742
743  /**
744   * Extracts the entryUUID attribute value from the provided list of
745   * attributes. If the attribute is not present one is generated from the DN
746   * using the same algorithm as the entryUUID virtual attribute provider.
747   */
748  private static String extractEntryUUID(List<Attribute> entryUUIDAttributes, DN entryDN)
749  {
750    if (entryUUIDAttributes != null)
751    {
752      Attribute uuidAttr = entryUUIDAttributes.get(0);
753      if (!uuidAttr.isEmpty())
754      {
755        return uuidAttr.iterator().next().toString();
756      }
757    }
758
759    // Generate a fake entryUUID: see OPENDJ-181. In rare pathological cases
760    // an entryUUID attribute may not be present and this causes severe side effects
761    // for replication which requires the attribute to always be present
762    if (logger.isTraceEnabled())
763    {
764      logger.trace(
765          "Replication requires an entryUUID attribute in order "
766              + "to perform conflict resolution, but none was "
767              + "found in entry \"%s\": generating virtual entryUUID instead",
768          entryDN);
769    }
770
771    return UUID.nameUUIDFromBytes(entryDN.toNormalizedByteString().toByteArray()).toString();
772  }
773}