/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. */ package org.opends.server.replication.plugin; import java.util.Iterator; import java.util.Set; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.schema.AttributeType; import org.opends.server.replication.common.CSN; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeBuilder; import org.opends.server.types.Entry; import org.opends.server.types.Modification; import com.forgerock.opendj.util.SmallSet; /** * This class is used to store historical information for multiple valued attributes. * One object of this type is created for each attribute that was changed in the entry. * It allows to record the last time a given value was added,the last * time a given value was deleted and the last time the whole attribute was deleted. */ public class AttrHistoricalMultiple extends AttrHistorical { /** Last time when the attribute was deleted. */ private CSN deleteTime; /** Last time the attribute was modified. */ private CSN lastUpdateTime; /** Change history for the values of this attribute. */ private final SmallSet valuesHist = new SmallSet<>(); /** * Create a new object from the provided information. * @param deleteTime the last time this attribute was deleted * @param updateTime the last time this attribute was updated * @param valuesHist the new attribute values when updated. */ AttrHistoricalMultiple(CSN deleteTime, CSN updateTime, Set valuesHist) { this.deleteTime = deleteTime; this.lastUpdateTime = updateTime; if (valuesHist != null) { this.valuesHist.addAll(valuesHist); } } /** Creates a new object. */ public AttrHistoricalMultiple() { this.deleteTime = null; this.lastUpdateTime = null; } /** * Returns the last time when the attribute was updated. * @return the last time when the attribute was updated */ CSN getLastUpdateTime() { return lastUpdateTime; } @Override public CSN getDeleteTime() { return deleteTime; } /** * Delete all historical information that is older than the provided CSN for * this attribute type. * Add the delete attribute state information * @param csn time when the delete was done */ void delete(CSN csn) { // iterate through the values in the valuesInfo and suppress all the values // that have not been added after the date of this delete. for (Iterator it = valuesHist.iterator(); it.hasNext();) { AttrValueHistorical info = it.next(); if (csn.isNewerThanOrEqualTo(info.getValueUpdateTime()) && csn.isNewerThanOrEqualTo(info.getValueDeleteTime())) { it.remove(); } } if (csn.isNewerThan(deleteTime)) { deleteTime = csn; } if (csn.isNewerThan(lastUpdateTime)) { lastUpdateTime = csn; } } /** * Update the historical of this attribute after deleting a set of values. * * @param attr * the attribute containing the set of values that were deleted * @param csn * time when the delete was done */ void delete(Attribute attr, CSN csn) { AttributeType attrType = attr.getAttributeDescription().getAttributeType(); for (ByteString val : attr) { delete(val, attrType, csn); } } /** * Update the historical of this attribute after a delete value. * * @param val * value that was deleted * @param attrType * @param csn * time when the delete was done */ void delete(ByteString val, AttributeType attrType, CSN csn) { update(csn, new AttrValueHistorical(val, attrType, null, csn)); } /** * Update the historical information when values are added. * * @param attr * the attribute containing the set of added values * @param csn * time when the add is done */ private void add(Attribute attr, CSN csn) { AttributeType attrType = attr.getAttributeDescription().getAttributeType(); for (ByteString val : attr) { add(val, attrType, csn); } } /** * Update the historical information when a value is added. * * @param addedValue * the added value * @param attrType * the attribute type of the added value * @param csn * time when the value was added */ void add(ByteString addedValue, AttributeType attrType, CSN csn) { update(csn, new AttrValueHistorical(addedValue, attrType, csn, null)); } private void update(CSN csn, AttrValueHistorical valInfo) { valuesHist.addOrReplace(valInfo); if (csn.isNewerThan(lastUpdateTime)) { lastUpdateTime = csn; } } @Override public Set getValuesHistorical() { return valuesHist; } @Override public boolean replayOperation(Iterator modsIterator, CSN csn, Entry modifiedEntry, Modification m) { if (csn.isNewerThanOrEqualTo(getLastUpdateTime()) && m.getModificationType() == ModificationType.REPLACE) { processLocalOrNonConflictModification(csn, m); return false;// the attribute was not modified more recently } // We are replaying an operation that was already done // on another master server and this operation has a potential // conflict with some more recent operations on this same entry // we need to take the more complex path to solve them return replayPotentialConflictModification(modsIterator, csn, modifiedEntry, m); } private boolean replayPotentialConflictModification(Iterator modsIterator, CSN csn, Entry modifiedEntry, Modification m) { // the attribute was modified after this change -> conflict switch (m.getModificationType().asEnum()) { case DELETE: if (csn.isOlderThan(getDeleteTime())) { /* this delete is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); return true; } if (!processDeleteConflict(csn, m, modifiedEntry)) { modsIterator.remove(); return true; } return false; case ADD: if (!processAddConflict(csn, m)) { modsIterator.remove(); return true; } return false; case REPLACE: if (csn.isOlderThan(getDeleteTime())) { /* this replace is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); return true; } /* save the values that are added by the replace operation into addedValues * first process the replace as a delete operation * -> this generates a list of values that should be kept * then process the addedValues as if they were coming from an add * -> this generates the list of values that needs to be added * concatenate the 2 generated lists into a replace */ boolean conflict = false; Attribute addedValues = m.getAttribute(); m.setAttribute(new AttributeBuilder(addedValues.getAttributeDescription()).toAttribute()); processDeleteConflict(csn, m, modifiedEntry); Attribute keptValues = m.getAttribute(); m.setAttribute(addedValues); if (!processAddConflict(csn, m)) { modsIterator.remove(); conflict = true; } AttributeBuilder builder = new AttributeBuilder(keptValues); builder.addAll(m.getAttribute()); m.setAttribute(builder.toAttribute()); return conflict; case INCREMENT: // TODO : FILL ME return false; default: return false; } } @Override public void processLocalOrNonConflictModification(CSN csn, Modification mod) { /* * The operation is either a non-conflicting operation or a local operation * so there is no need to check the historical information for conflicts. * If this is a local operation, then this code is run after * the pre-operation phase. * If this is a non-conflicting replicated operation, this code is run * during the handleConflictResolution(). */ Attribute modAttr = mod.getAttribute(); AttributeType type = modAttr.getAttributeDescription().getAttributeType(); switch (mod.getModificationType().asEnum()) { case DELETE: if (modAttr.isEmpty()) { delete(csn); } else { delete(modAttr, csn); } break; case ADD: if (type.isSingleValue()) { delete(csn); } add(modAttr, csn); break; case REPLACE: /* TODO : can we replace specific attribute values ????? */ delete(csn); add(modAttr, csn); break; case INCREMENT: /* FIXME : we should update CSN */ break; } } /** * Process a delete attribute values that is conflicting with a previous modification. * * @param csn The CSN of the currently processed change * @param m the modification that is being processed * @param modifiedEntry the entry that is modified (before current mod) * @return {@code true} if no conflict was detected, {@code false} otherwise. */ private boolean processDeleteConflict(CSN csn, Modification m, Entry modifiedEntry) { /* * We are processing a conflicting DELETE modification * * This code is written on the assumption that conflict are * rare. We therefore don't care much about the performance * However since it is rarely executed this code needs to be * as simple as possible to make sure that all paths are tested. * In this case the most simple seem to change the DELETE * in a REPLACE modification that keeps all values * more recent that the DELETE. * we are therefore going to change m into a REPLACE that will keep * all the values that have been updated after the DELETE time * If a value is present in the entry without any state information * it must be removed so we simply ignore them */ Attribute modAttr = m.getAttribute(); if (modAttr.isEmpty()) { // We are processing a DELETE attribute modification m.setModificationType(ModificationType.REPLACE); AttributeBuilder builder = new AttributeBuilder(modAttr.getAttributeDescription()); for (Iterator it = valuesHist.iterator(); it.hasNext();) { AttrValueHistorical valInfo = it.next(); if (csn.isOlderThan(valInfo.getValueUpdateTime())) { // this value has been updated after this delete, // therefore this value must be kept builder.add(valInfo.getAttributeValue()); } else if (csn.isNewerThanOrEqualTo(valInfo.getValueDeleteTime())) { /* * this value is going to be deleted, remove it from historical * information unless it is a Deleted attribute value that is * more recent than this DELETE */ it.remove(); } } m.setAttribute(builder.toAttribute()); if (csn.isNewerThan(getDeleteTime())) { deleteTime = csn; } if (csn.isNewerThan(getLastUpdateTime())) { lastUpdateTime = csn; } } else { // we are processing DELETE of some attribute values AttributeBuilder builder = new AttributeBuilder(modAttr); AttributeType attrType = modAttr.getAttributeDescription().getAttributeType(); for (ByteString val : modAttr) { boolean deleteIt = true; // true if the delete must be done boolean addedInCurrentOp = false; // update historical information AttrValueHistorical valInfo = new AttrValueHistorical(val, attrType, null, csn); AttrValueHistorical oldValInfo = valuesHist.get(valInfo); if (oldValInfo == null) { valuesHist.add(valInfo); } else { // this value already exist in the historical information if (csn.equals(oldValInfo.getValueUpdateTime())) { // This value was added earlier in the same operation // we need to keep the delete. addedInCurrentOp = true; } if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime()) && csn.isNewerThanOrEqualTo(oldValInfo.getValueUpdateTime())) { valuesHist.addOrReplace(valInfo); } else if (oldValInfo.isUpdate()) { deleteIt = false; } } /* if the attribute value is not to be deleted * or if attribute value is not present suppress it from the * MOD to make sure the delete is going to succeed */ if (!deleteIt || (!modifiedEntry.hasValue(modAttr.getAttributeDescription(), val) && ! addedInCurrentOp)) { // this value was already deleted before and therefore // this should not be replayed. builder.remove(val); if (builder.isEmpty()) { // This was the last values in the set of values to be deleted. // this MOD must therefore be skipped. return false; } } } m.setAttribute(builder.toAttribute()); if (csn.isNewerThan(getLastUpdateTime())) { lastUpdateTime = csn; } } return true; } /** * Process a add attribute values that is conflicting with a previous modification. * * @param csn * the historical info associated to the entry * @param m * the modification that is being processed * @return {@code true} if no conflict was detected, {@code false} otherwise. */ private boolean processAddConflict(CSN csn, Modification m) { /* * if historicalattributedelete is newer forget this mod else find * attr value if does not exist add historicalvalueadded timestamp * add real value in entry else if timestamp older and already was * historicalvalueadded update historicalvalueadded else if * timestamp older and was historicalvaluedeleted change * historicalvaluedeleted into historicalvalueadded add value in * real entry */ if (csn.isOlderThan(getDeleteTime())) { /* A delete has been done more recently than this add * forget this MOD ADD */ return false; } Attribute attribute = m.getAttribute(); AttributeBuilder builder = new AttributeBuilder(attribute); AttributeType attrType = attribute.getAttributeDescription().getAttributeType(); for (ByteString addVal : attribute) { AttrValueHistorical valInfo = new AttrValueHistorical(addVal, attrType, csn, null); AttrValueHistorical oldValInfo = valuesHist.get(valInfo); if (oldValInfo == null) { /* this value does not exist yet * add it in the historical information * let the operation process normally */ valuesHist.add(valInfo); } else { if (oldValInfo.isUpdate()) { /* if the value is already present * check if the updateTime must be updated * in all cases suppress this value from the value list * as it is already present in the entry */ if (csn.isNewerThan(oldValInfo.getValueUpdateTime())) { valuesHist.addOrReplace(valInfo); } builder.remove(addVal); } else { // it is a delete /* this value is marked as a deleted value * check if this mod is more recent the this delete */ if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime())) { valuesHist.addOrReplace(valInfo); } else { /* the delete that is present in the historical information * is more recent so it must win, * remove this value from the list of values to add * don't update the historical information */ builder.remove(addVal); } } } } Attribute attr = builder.toAttribute(); m.setAttribute(attr); if (attr.isEmpty()) { return false; } if (csn.isNewerThan(getLastUpdateTime())) { lastUpdateTime = csn; } return true; } @Override public void assign(HistoricalAttributeValue histVal) { final ByteString value = histVal.getAttributeValue(); final AttributeType attrType = histVal.getAttributeDescription().getAttributeType(); final CSN csn = histVal.getCSN(); switch (histVal.getHistKey()) { case ADD: if (value != null) { add(value, attrType, csn); } break; case DEL: if (value != null) { delete(value, attrType, csn); } break; case REPL: delete(csn); if (value != null) { add(value, attrType, csn); } break; case ATTRDEL: delete(csn); break; } } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()).append("("); boolean deleteAppended = false; if (deleteTime != null) { deleteAppended = true; sb.append("deleteTime=").append(deleteTime); } if (lastUpdateTime != null) { if (deleteAppended) { sb.append(", "); } sb.append("lastUpdateTime=").append(lastUpdateTime); } sb.append(", valuesHist=").append(valuesHist); sb.append(")"); return sb.toString(); } }