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 java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.Map;
032import java.util.Set;
033
034import org.forgerock.opendj.ldap.ByteString;
035import org.forgerock.opendj.ldap.ModificationType;
036import org.opends.server.replication.common.CSN;
037import org.opends.server.types.Attribute;
038import org.opends.server.types.AttributeBuilder;
039import org.opends.server.types.AttributeType;
040import org.opends.server.types.Entry;
041import org.opends.server.types.Modification;
042
043/**
044 * This class is used to store historical information for multiple valued attributes.
045 * One object of this type is created for each attribute that was changed in the entry.
046 * It allows to record the last time a given value was added,the last
047 * time a given value was deleted and the last time the whole attribute was deleted.
048 */
049public class AttrHistoricalMultiple extends AttrHistorical
050{
051  /** Last time when the attribute was deleted. */
052  private CSN deleteTime;
053  /** Last time the attribute was modified. */
054  private CSN lastUpdateTime;
055  /**
056   * Change history for the values of this attribute.
057   * <p>
058   * We are using a LinkedHashMap here because we want:
059   * <ol>
060   * <li>Fast access for removing/adding a AttrValueHistorical keyed by the attribute value => Use a Map</li>
061   * <li>Ordering changes according to the CSN of each changes => Use a LinkedHashMap</li>
062   * </ol>
063   */
064  private final Map<AttrValueHistorical, AttrValueHistorical> valuesHist = new LinkedHashMap<>();
065
066   /**
067    * Create a new object from the provided information.
068    * @param deleteTime the last time this attribute was deleted
069    * @param updateTime the last time this attribute was updated
070    * @param valuesHist the new attribute values when updated.
071    */
072   public AttrHistoricalMultiple(CSN deleteTime,
073       CSN updateTime,
074       Map<AttrValueHistorical,AttrValueHistorical> valuesHist)
075   {
076     this.deleteTime = deleteTime;
077     this.lastUpdateTime = updateTime;
078     if (valuesHist != null)
079     {
080       this.valuesHist.putAll(valuesHist);
081     }
082   }
083
084   /** Creates a new object. */
085   public AttrHistoricalMultiple()
086   {
087     this.deleteTime = null;
088     this.lastUpdateTime = null;
089   }
090
091   /**
092    * Returns the last time when the attribute was updated.
093    * @return the last time when the attribute was updated
094    */
095   private CSN getLastUpdateTime()
096   {
097     return lastUpdateTime;
098   }
099
100   @Override
101   public CSN getDeleteTime()
102   {
103     return deleteTime;
104   }
105
106   /**
107    * Duplicate an object. CSNs are duplicated by references.
108    * <p>
109    * Method only called in tests
110    *
111    * @return the duplicated object.
112    */
113   AttrHistoricalMultiple duplicate()
114   {
115     return new AttrHistoricalMultiple(this.deleteTime, this.lastUpdateTime, this.valuesHist);
116   }
117
118   /**
119    * Delete all historical information that is older than the provided CSN for
120    * this attribute type.
121    * Add the delete attribute state information
122    * @param csn time when the delete was done
123    */
124   void delete(CSN csn)
125   {
126     // iterate through the values in the valuesInfo and suppress all the values
127     // that have not been added after the date of this delete.
128     Iterator<AttrValueHistorical> it = valuesHist.keySet().iterator();
129     while (it.hasNext())
130     {
131       AttrValueHistorical info = it.next();
132       if (csn.isNewerThanOrEqualTo(info.getValueUpdateTime()) &&
133           csn.isNewerThanOrEqualTo(info.getValueDeleteTime()))
134      {
135        it.remove();
136      }
137     }
138
139     if (csn.isNewerThan(deleteTime))
140     {
141       deleteTime = csn;
142     }
143
144     if (csn.isNewerThan(lastUpdateTime))
145     {
146       lastUpdateTime = csn;
147     }
148   }
149
150  /**
151   * Update the historical of this attribute after deleting a set of values.
152   *
153   * @param attr
154   *          the attribute containing the set of values that were deleted
155   * @param csn
156   *          time when the delete was done
157   */
158  void delete(Attribute attr, CSN csn)
159  {
160    for (ByteString val : attr)
161    {
162      delete(val, csn);
163    }
164  }
165
166   /**
167    * Update the historical of this attribute after a delete value.
168    *
169    * @param val value that was deleted
170    * @param csn time when the delete was done
171    */
172   void delete(ByteString val, CSN csn)
173   {
174     update(csn, new AttrValueHistorical(val, null, csn));
175   }
176
177  /**
178   * Update the historical information when values are added.
179   *
180   * @param attr
181   *          the attribute containing the set of added values
182   * @param csn
183   *          time when the add is done
184   */
185  private void add(Attribute attr, CSN csn)
186  {
187    for (ByteString val : attr)
188    {
189      add(val, csn);
190    }
191  }
192
193   /**
194     * Update the historical information when a value is added.
195     *
196     * @param addedValue
197     *          values that was added
198     * @param csn
199     *          time when the value was added
200     */
201   void add(ByteString addedValue, CSN csn)
202   {
203     update(csn, new AttrValueHistorical(addedValue, csn, null));
204   }
205
206  private void update(CSN csn, AttrValueHistorical valInfo)
207  {
208    updateValInfo(valInfo, valInfo);
209    if (csn.isNewerThan(lastUpdateTime))
210    {
211      lastUpdateTime = csn;
212    }
213  }
214
215  private void updateValInfo(AttrValueHistorical oldValInfo, AttrValueHistorical newValInfo)
216  {
217    valuesHist.remove(oldValInfo);
218    valuesHist.put(newValInfo, newValInfo);
219  }
220
221  @Override
222  public Set<AttrValueHistorical> getValuesHistorical()
223  {
224    return valuesHist.keySet();
225  }
226
227  @Override
228  public boolean replayOperation(Iterator<Modification> modsIterator, CSN csn,
229      Entry modifiedEntry, Modification m)
230  {
231    if (csn.isNewerThanOrEqualTo(getLastUpdateTime())
232        && m.getModificationType() == ModificationType.REPLACE)
233    {
234      processLocalOrNonConflictModification(csn, m);
235      return false;// the attribute was not modified more recently
236    }
237    // We are replaying an operation that was already done
238    // on another master server and this operation has a potential
239    // conflict with some more recent operations on this same entry
240    // we need to take the more complex path to solve them
241    return replayPotentialConflictModification(modsIterator, csn, modifiedEntry, m);
242  }
243
244  private boolean replayPotentialConflictModification(Iterator<Modification> modsIterator, CSN csn,
245      Entry modifiedEntry, Modification m)
246  {
247    // the attribute was modified after this change -> conflict
248    switch (m.getModificationType().asEnum())
249    {
250    case DELETE:
251      if (csn.isOlderThan(getDeleteTime()))
252      {
253        /* this delete is already obsoleted by a more recent delete
254         * skip this mod
255         */
256        modsIterator.remove();
257        return true;
258      }
259
260      if (!processDeleteConflict(csn, m, modifiedEntry))
261      {
262        modsIterator.remove();
263        return true;
264      }
265      return false;
266
267    case ADD:
268      if (!processAddConflict(csn, m))
269      {
270        modsIterator.remove();
271        return true;
272      }
273      return false;
274
275    case REPLACE:
276      if (csn.isOlderThan(getDeleteTime()))
277      {
278        /* this replace is already obsoleted by a more recent delete
279         * skip this mod
280         */
281        modsIterator.remove();
282        return true;
283      }
284
285      /* save the values that are added by the replace operation into addedValues
286       * first process the replace as a delete operation
287       * -> this generates a list of values that should be kept
288       * then process the addedValues as if they were coming from an add
289       * -> this generates the list of values that needs to be added
290       * concatenate the 2 generated lists into a replace
291       */
292      boolean conflict = false;
293      Attribute addedValues = m.getAttribute();
294      m.setAttribute(new AttributeBuilder(addedValues, true).toAttribute());
295
296      processDeleteConflict(csn, m, modifiedEntry);
297      Attribute keptValues = m.getAttribute();
298
299      m.setAttribute(addedValues);
300      if (!processAddConflict(csn, m))
301      {
302        modsIterator.remove();
303        conflict = true;
304      }
305
306      AttributeBuilder builder = new AttributeBuilder(keptValues);
307      builder.addAll(m.getAttribute());
308      m.setAttribute(builder.toAttribute());
309      return conflict;
310
311    case INCREMENT:
312      // TODO : FILL ME
313      return false;
314
315    default:
316      return false;
317    }
318  }
319
320  @Override
321  public void processLocalOrNonConflictModification(CSN csn, Modification mod)
322  {
323    /*
324     * The operation is either a non-conflicting operation or a local operation
325     * so there is no need to check the historical information for conflicts.
326     * If this is a local operation, then this code is run after
327     * the pre-operation phase.
328     * If this is a non-conflicting replicated operation, this code is run
329     * during the handleConflictResolution().
330     */
331
332    Attribute modAttr = mod.getAttribute();
333    AttributeType type = modAttr.getAttributeType();
334
335    switch (mod.getModificationType().asEnum())
336    {
337    case DELETE:
338      if (modAttr.isEmpty())
339      {
340        delete(csn);
341      }
342      else
343      {
344        delete(modAttr, csn);
345      }
346      break;
347
348    case ADD:
349      if (type.isSingleValue())
350      {
351        delete(csn);
352      }
353      add(modAttr, csn);
354      break;
355
356    case REPLACE:
357      /* TODO : can we replace specific attribute values ????? */
358      delete(csn);
359      add(modAttr, csn);
360      break;
361
362    case INCREMENT:
363      /* FIXME : we should update CSN */
364      break;
365    }
366  }
367
368  /**
369   * Process a delete attribute values that is conflicting with a previous modification.
370   *
371   * @param csn The CSN of the currently processed change
372   * @param m the modification that is being processed
373   * @param modifiedEntry the entry that is modified (before current mod)
374   * @return {@code true} if no conflict was detected, {@code false} otherwise.
375   */
376  private boolean processDeleteConflict(CSN csn, Modification m, Entry modifiedEntry)
377  {
378    /*
379     * We are processing a conflicting DELETE modification
380     *
381     * This code is written on the assumption that conflict are
382     * rare. We therefore don't care much about the performance
383     * However since it is rarely executed this code needs to be
384     * as simple as possible to make sure that all paths are tested.
385     * In this case the most simple seem to change the DELETE
386     * in a REPLACE modification that keeps all values
387     * more recent that the DELETE.
388     * we are therefore going to change m into a REPLACE that will keep
389     * all the values that have been updated after the DELETE time
390     * If a value is present in the entry without any state information
391     * it must be removed so we simply ignore them
392     */
393
394    Attribute modAttr = m.getAttribute();
395    if (modAttr.isEmpty())
396    {
397      // We are processing a DELETE attribute modification
398      m.setModificationType(ModificationType.REPLACE);
399      AttributeBuilder builder = new AttributeBuilder(modAttr, true);
400
401      for (Iterator<AttrValueHistorical> it = valuesHist.keySet().iterator(); it.hasNext();)
402      {
403        AttrValueHistorical valInfo = it.next();
404
405        if (csn.isOlderThan(valInfo.getValueUpdateTime()))
406        {
407          // this value has been updated after this delete,
408          // therefore this value must be kept
409          builder.add(valInfo.getAttributeValue());
410        }
411        else if (csn.isNewerThanOrEqualTo(valInfo.getValueDeleteTime()))
412        {
413          /*
414           * this value is going to be deleted, remove it from historical
415           * information unless it is a Deleted attribute value that is
416           * more recent than this DELETE
417           */
418          it.remove();
419        }
420      }
421
422      m.setAttribute(builder.toAttribute());
423
424      if (csn.isNewerThan(getDeleteTime()))
425      {
426        deleteTime = csn;
427      }
428      if (csn.isNewerThan(getLastUpdateTime()))
429      {
430        lastUpdateTime = csn;
431      }
432    }
433    else
434    {
435      // we are processing DELETE of some attribute values
436      AttributeBuilder builder = new AttributeBuilder(modAttr);
437
438      for (ByteString val : modAttr)
439      {
440        boolean deleteIt = true;  // true if the delete must be done
441        boolean addedInCurrentOp = false;
442
443        // update historical information
444        AttrValueHistorical valInfo = new AttrValueHistorical(val, null, csn);
445        AttrValueHistorical oldValInfo = valuesHist.get(valInfo);
446        if (oldValInfo != null)
447        {
448          // this value already exist in the historical information
449          if (csn.equals(oldValInfo.getValueUpdateTime()))
450          {
451            // This value was added earlier in the same operation
452            // we need to keep the delete.
453            addedInCurrentOp = true;
454          }
455          if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime()) &&
456              csn.isNewerThanOrEqualTo(oldValInfo.getValueUpdateTime()))
457          {
458            updateValInfo(oldValInfo, valInfo);
459          }
460          else if (oldValInfo.isUpdate())
461          {
462            deleteIt = false;
463          }
464        }
465        else
466        {
467          updateValInfo(oldValInfo, valInfo);
468        }
469
470        /* if the attribute value is not to be deleted
471         * or if attribute value is not present suppress it from the
472         * MOD to make sure the delete is going to succeed
473         */
474        if (!deleteIt
475            || (!modifiedEntry.hasValue(modAttr.getAttributeType(), modAttr
476                .getOptions(), val) && ! addedInCurrentOp))
477        {
478          // this value was already deleted before and therefore
479          // this should not be replayed.
480          builder.remove(val);
481          if (builder.isEmpty())
482          {
483            // This was the last values in the set of values to be deleted.
484            // this MOD must therefore be skipped.
485            return false;
486          }
487        }
488      }
489
490      m.setAttribute(builder.toAttribute());
491
492      if (csn.isNewerThan(getLastUpdateTime()))
493      {
494        lastUpdateTime = csn;
495      }
496    }
497
498    return true;
499  }
500
501  /**
502   * Process a add attribute values that is conflicting with a previous modification.
503   *
504   * @param csn
505   *          the historical info associated to the entry
506   * @param m
507   *          the modification that is being processed
508   * @return {@code true} if no conflict was detected, {@code false} otherwise.
509   */
510  private boolean processAddConflict(CSN csn, Modification m)
511  {
512    /*
513     * if historicalattributedelete is newer forget this mod else find
514     * attr value if does not exist add historicalvalueadded timestamp
515     * add real value in entry else if timestamp older and already was
516     * historicalvalueadded update historicalvalueadded else if
517     * timestamp older and was historicalvaluedeleted change
518     * historicalvaluedeleted into historicalvalueadded add value in
519     * real entry
520     */
521
522    if (csn.isOlderThan(getDeleteTime()))
523    {
524      /* A delete has been done more recently than this add
525       * forget this MOD ADD
526       */
527      return false;
528    }
529
530    AttributeBuilder builder = new AttributeBuilder(m.getAttribute());
531    for (ByteString addVal : m.getAttribute())
532    {
533      AttrValueHistorical valInfo = new AttrValueHistorical(addVal, csn, null);
534      AttrValueHistorical oldValInfo = valuesHist.get(valInfo);
535      if (oldValInfo == null)
536      {
537        /* this value does not exist yet
538         * add it in the historical information
539         * let the operation process normally
540         */
541        valuesHist.put(valInfo, valInfo);
542      }
543      else
544      {
545        if  (oldValInfo.isUpdate())
546        {
547          /* if the value is already present
548           * check if the updateTime must be updated
549           * in all cases suppress this value from the value list
550           * as it is already present in the entry
551           */
552          if (csn.isNewerThan(oldValInfo.getValueUpdateTime()))
553          {
554            updateValInfo(oldValInfo, valInfo);
555          }
556          builder.remove(addVal);
557        }
558        else
559        { // it is a delete
560          /* this value is marked as a deleted value
561           * check if this mod is more recent the this delete
562           */
563          if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime()))
564          {
565            updateValInfo(oldValInfo, valInfo);
566          }
567          else
568          {
569            /* the delete that is present in the historical information
570             * is more recent so it must win,
571             * remove this value from the list of values to add
572             * don't update the historical information
573             */
574            builder.remove(addVal);
575          }
576        }
577      }
578    }
579
580    Attribute attr = builder.toAttribute();
581    m.setAttribute(attr);
582
583    if (attr.isEmpty())
584    {
585      return false;
586    }
587
588    if (csn.isNewerThan(getLastUpdateTime()))
589    {
590      lastUpdateTime = csn;
591    }
592    return true;
593  }
594
595  @Override
596  public void assign(HistAttrModificationKey histKey, ByteString value, CSN csn)
597  {
598    switch (histKey)
599    {
600    case ADD:
601      if (value != null)
602      {
603        add(value, csn);
604      }
605      break;
606
607    case DEL:
608      if (value != null)
609      {
610        delete(value, csn);
611      }
612      break;
613
614    case REPL:
615      delete(csn);
616      if (value != null)
617      {
618        add(value, csn);
619      }
620      break;
621
622    case ATTRDEL:
623      delete(csn);
624      break;
625    }
626  }
627
628  @Override
629  public String toString()
630  {
631    final StringBuilder sb = new StringBuilder();
632    sb.append(getClass().getSimpleName()).append("(");
633    boolean deleteAppended = false;
634    if (deleteTime != null)
635    {
636      deleteAppended = true;
637      sb.append("deleteTime=").append(deleteTime);
638    }
639    if (lastUpdateTime != null)
640    {
641      if (deleteAppended)
642      {
643        sb.append(", ");
644      }
645      sb.append("lastUpdateTime=").append(lastUpdateTime);
646    }
647    sb.append(", valuesHist=").append(valuesHist.keySet());
648    sb.append(")");
649    return sb.toString();
650  }
651}