opends/src/server/org/opends/server/synchronization/plugin/HistVal.java
@@ -89,7 +89,7 @@ String[] optionsToken = token[0].split(";"); options = new HashSet<String>(); int index = 1; while (optionsToken[index] != null) while (index < optionsToken.length) { options.add(optionsToken[index]); index ++; opends/src/server/org/opends/server/synchronization/plugin/Historical.java
@@ -96,6 +96,17 @@ = new HashMap<AttributeType,AttrInfoWithOptions>(); /** * {@inheritDoc} */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(encode()); return builder.toString(); } /** * Duplicates an Historical Object. * attributesInfo are nor duplicated but used as references. * @return The duplicate of the Historical Object @@ -131,12 +142,16 @@ Attribute modAttr = m.getAttribute(); Set<String> options = modAttr.getOptions(); if (options.isEmpty()) { options = null; } AttributeType type = modAttr.getAttributeType(); AttrInfoWithOptions attrInfoWithOptions = attributesInfo.get(type); AttrInfo attrInfo = null; if (attrInfoWithOptions != null) { attrInfo = attrInfoWithOptions.get(options); } if (this.hasConflict(attrInfo, changeNumber)) { @@ -148,68 +163,72 @@ switch (m.getModificationType()) { case DELETE: if (changeNumber.older(attrInfo.getDeleteTime())) { /* this delete is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); case DELETE: if (changeNumber.older(attrInfo.getDeleteTime())) { /* this delete is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); break; } this.conflictDelete(changeNumber, type, m, modifiedEntry, attrInfo, modAttr); break; } this.conflictDelete(changeNumber, type, m, modifiedEntry, attrInfo, modAttr); break; case ADD: this.conflictAdd(modsIterator, changeNumber, attrInfo, modAttr.getValues(), modAttr.getOptions()); break; case REPLACE: if (changeNumber.older(attrInfo.getDeleteTime())) { /* this replace is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); case ADD: this.conflictAdd(modsIterator, changeNumber, attrInfo, modAttr.getValues(), modAttr.getOptions()); break; } /* save the values that are added by the replace operation * into addedValues * first process the replace as a delete operation -> this generate * a list of values that should be kept * then process the addedValues as if they were coming from a add * -> this generate the list of values that needs to be added * concatenate the 2 generated lists into a replace */ LinkedHashSet<AttributeValue> addedValues = modAttr.getValues(); modAttr.setValues(new LinkedHashSet<AttributeValue>()); this.conflictDelete(changeNumber, type, m, modifiedEntry, attrInfo, modAttr); case REPLACE: if (changeNumber.older(attrInfo.getDeleteTime())) { /* this replace is already obsoleted by a more recent delete * skip this mod */ modsIterator.remove(); break; } /* save the values that are added by the replace operation * into addedValues * first process the replace as a delete operation -> this generate * a list of values that should be kept * then process the addedValues as if they were coming from a add * -> this generate the list of values that needs to be added * concatenate the 2 generated lists into a replace */ LinkedHashSet<AttributeValue> addedValues = modAttr.getValues(); modAttr.setValues(new LinkedHashSet<AttributeValue>()); LinkedHashSet<AttributeValue> keptValues = modAttr.getValues(); this.conflictAdd(modsIterator, changeNumber, attrInfo, addedValues, modAttr.getOptions()); keptValues.addAll(addedValues); break; this.conflictDelete(changeNumber, type, m, modifiedEntry, attrInfo, modAttr); case INCREMENT: // TODO : FILL ME break; LinkedHashSet<AttributeValue> keptValues = modAttr.getValues(); this.conflictAdd(modsIterator, changeNumber, attrInfo, addedValues, modAttr.getOptions()); keptValues.addAll(addedValues); break; case INCREMENT: // TODO : FILL ME break; } } else { processLocalOrNonConflictModification(changeNumber, m); } } // TODO : now purge old historical information if (moreRecentChangenumber == null || moreRecentChangenumber.older(changeNumber)) moreRecentChangenumber.older(changeNumber)) { moreRecentChangenumber = changeNumber; } } /** @@ -240,17 +259,25 @@ Attribute modAttr = mod.getAttribute(); if (modAttr.getAttributeType().equals(historicalAttrType)) { return; } Set<String> options = modAttr.getOptions(); if (options.isEmpty()) { options = null; } AttributeType type = modAttr.getAttributeType(); AttrInfoWithOptions attrInfoWithOptions = attributesInfo.get(type); AttrInfo attrInfo; if (attrInfoWithOptions != null) { attrInfo = attrInfoWithOptions.get(options); } else { attrInfo = null; } /* * The following code only works for multi-valued attributes. @@ -260,7 +287,9 @@ { attrInfo = new AttrInfo(); if (attrInfoWithOptions == null) { attrInfoWithOptions = new AttrInfoWithOptions(); } attrInfoWithOptions.put(options, attrInfo); attributesInfo.put(type, attrInfoWithOptions); } @@ -268,9 +297,13 @@ { case DELETE: if (modAttr.getValues().isEmpty()) { attrInfo.delete(changeNumber); } else { attrInfo.delete(modAttr.getValues(), changeNumber); } break; case ADD: @@ -319,10 +352,26 @@ processLocalOrNonConflictModification(changeNumber, mod); } if (moreRecentChangenumber == null || moreRecentChangenumber.older(changeNumber)) moreRecentChangenumber.older(changeNumber)) { moreRecentChangenumber = changeNumber; } } Attribute attr = encode(); Modification mod; mod = new Modification(ModificationType.REPLACE, attr); mods.add(mod); modifiedEntry.removeAttribute(historicalAttrType); modifiedEntry.addAttribute(attr, null); } /** * Encode the historical information in an operational attribute. * @return The historical information encoded in an operational attribute. */ public Attribute encode() { LinkedHashSet<AttributeValue> hist = new LinkedHashSet<AttributeValue>(); for (Map.Entry<AttributeType, AttrInfoWithOptions> entryWithOptions : @@ -330,7 +379,7 @@ { AttributeType type = entryWithOptions.getKey(); HashMap<Set<String> ,AttrInfo> attrwithoptions = HashMap<Set<String> , AttrInfo> attrwithoptions = entryWithOptions.getValue().getAttributesInfo(); for (Map.Entry<Set<String>, AttrInfo> entry : attrwithoptions.entrySet()) @@ -342,8 +391,15 @@ ChangeNumber deleteTime = info.getDeleteTime(); if (options != null) { StringBuilder optionsBuilder = new StringBuilder(); for (String s : options) optionsString.concat(";"+s); { optionsBuilder.append(';'); optionsBuilder.append(s); } optionsString = optionsBuilder.toString(); } /* generate the historical information for deleted attributes */ if (deleteTime != null) @@ -391,7 +447,6 @@ String strValue = type.getNormalizedPrimaryName() + optionsString + ":" + deleteTime.toString() + ":attrDel"; delAttr = false; AttributeValue val = new AttributeValue(historicalAttrType, strValue); hist.add(val); } @@ -399,16 +454,16 @@ } Attribute attr; Modification mod; if (hist.isEmpty()) { attr = new Attribute(historicalAttrType, HISTORICALATTRIBUTENAME, null); } else { attr = new Attribute(historicalAttrType, HISTORICALATTRIBUTENAME, hist); mod = new Modification(ModificationType.REPLACE, attr); mods.add(mod); modifiedEntry.removeAttribute(historicalAttrType); modifiedEntry.addAttribute(attr, null); } return attr; } /** @@ -428,15 +483,22 @@ if (ChangeNumber.compare(newChange, moreRecentChangenumber) <= 0) { if (info == null) { return false; // the attribute was never modified -> no conflict else if (ChangeNumber.compare(newChange, info.getLastUpdateTime()) <= 0) } else if (ChangeNumber.compare(newChange, info.getLastUpdateTime()) <= 0) { return true; // the attribute was modified after this change -> conflict } else { return false;// the attribute was not modified more recently } } else { return false; } } /** @@ -515,9 +577,13 @@ modAttr.setValues(replValues); if (changeNumber.newer(attrInfo.getDeleteTime())) { attrInfo.setDeleteTime(changeNumber); } if (changeNumber.newer(attrInfo.getLastUpdateTime())) { attrInfo.setLastUpdateTime(changeNumber); } } else { @@ -566,7 +632,9 @@ } } if (changeNumber.newer(attrInfo.getLastUpdateTime())) { attrInfo.setLastUpdateTime(changeNumber); } } return true; } @@ -668,10 +736,14 @@ } } if (addValues.isEmpty()) { modsIterator.remove(); } if (changeNumber.newer(attrInfo.getLastUpdateTime())) { attrInfo.setLastUpdateTime(changeNumber); } return true; } @@ -691,7 +763,9 @@ AttrInfoWithOptions attrInfoWithOptions = null; if (hist == null) { return histObj; } for (Attribute attr : hist) { @@ -734,8 +808,7 @@ } else { attrType = lastAttrType; if (options != lastOptions) if (!options.equals(lastOptions)) { attrInfo = new AttrInfo(); attrInfoWithOptions.put(options, attrInfo); @@ -782,6 +855,8 @@ /* set the reference to the historical information in the entry */ return histObj; } /** * Use this historical information to generate fake operations that would * result in this historical information. opends/src/server/org/opends/server/util/LDIFReader.java
@@ -983,8 +983,8 @@ * entry (used for writing reject information). * @param line The line to decode. * @param entryDN The DN of the entry being decoded. * @param attributeName The name of the attribute to return * the values for. * @param attributeName The name and options of the attribute to * return the values for. * * @return The attribute in octet string form. * @throws LDIFException If a problem occurs while trying to decode @@ -1007,13 +1007,17 @@ String attrDescr = line.substring(0, colonPos); Attribute attribute = parseAttrDescription(attrDescr); String attrName = attribute.getName(); String lowerName = toLowerCase(attrName); if(attributeName != null && !toLowerCase(attributeName).equals(lowerName)) if (attributeName != null) { int msgID = MSGID_LDIF_INVALID_CHANGERECORD_ATTRIBUTE; String message = getMessage(msgID, lowerName, attributeName); throw new LDIFException(msgID, message, lastEntryLineNumber, false); Attribute expectedAttr = parseAttrDescription(attributeName); if (!attribute.equals(expectedAttr)) { int msgID = MSGID_LDIF_INVALID_CHANGERECORD_ATTRIBUTE; String message = getMessage(msgID, attrDescr, attributeName); throw new LDIFException(msgID, message, lastEntryLineNumber, false); } } // Now parse the attribute value. opends/tests/unit-tests-testng/src/server/org/opends/server/synchronization/plugin/HistoricalTest.java
New file @@ -0,0 +1,166 @@ /* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.synchronization.plugin; import org.opends.server.synchronization.SynchronizationTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.tools.LDAPModify; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.Attribute; import org.opends.server.core.DirectoryServer; import org.testng.annotations.Test; import org.testng.annotations.BeforeClass; import static org.testng.Assert.assertEquals; import java.util.List; /** * Tests the Historical class. */ public class HistoricalTest extends SynchronizationTestCase { /** * Set up synchronization on the test backend. * @throws Exception If an error occurs. */ @BeforeClass @Override public void setUp() throws Exception { TestCaseUtils.initializeTestBackend(true); // Create an internal connection. connection = InternalClientConnection.getRootConnection(); // Top level synchronization provider. String synchroStringDN = "cn=Synchronization Providers,cn=config"; // Multimaster synchronization plugin. synchroPluginStringDN = "cn=Multimaster Synchronization, " + synchroStringDN; String synchroPluginLdif = "dn: " + synchroPluginStringDN + "\n" + "objectClass: top\n" + "objectClass: ds-cfg-synchronization-provider\n" + "ds-cfg-synchronization-provider-enabled: true\n" + "ds-cfg-synchronization-provider-class: " + "org.opends.server.synchronization.MultimasterSynchronization\n"; synchroPluginEntry = TestCaseUtils.entryFromLdifString(synchroPluginLdif); // The synchronization server. String changeLogStringDN = "cn=Changelog Server, " + synchroPluginStringDN; String changeLogLdif = "dn: " + changeLogStringDN + "\n" + "objectClass: top\n" + "objectClass: ds-cfg-synchronization-changelog-server-config\n" + "cn: Changelog Server\n" + "ds-cfg-changelog-port: 8989\n" + "ds-cfg-changelog-server-id: 1\n"; changeLogEntry = TestCaseUtils.entryFromLdifString(changeLogLdif); // The suffix to be synchronized. String synchroServerStringDN = "o=test, " + synchroPluginStringDN; String synchroServerLdif = "dn: " + synchroServerStringDN + "\n" + "objectClass: top\n" + "objectClass: ds-cfg-synchronization-provider-config\n" + "cn: example\n" + "ds-cfg-synchronization-dn: o=test\n" + "ds-cfg-changelog-server: localhost:8989\n" + "ds-cfg-directory-server-id: 1\n" + "ds-cfg-receive-status: true\n"; synchroServerEntry = TestCaseUtils.entryFromLdifString(synchroServerLdif); configureSynchronization(); } /** * Tests that the attribute modification history is correctly read from * and written to an operational attribute of the entry. * @throws Exception If the test fails. */ @Test public void testEncoding() throws Exception { // Add a test entry. TestCaseUtils.addEntry( "dn: uid=user.1,o=test", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: user.1", "cn: Aaccf Amar", "sn: Amar", "givenName: Aaccf", "userPassword: password", "description: Initial description" ); // Modify the test entry to give it some history. String path = TestCaseUtils.createTempFile( "dn: uid=user.1,o=test", "changetype: modify", "add: cn;lang-en", "cn;lang-en: Aaccf Amar", "cn;lang-en: Aaccf A Amar", "-", "replace: description", "description: replaced description", "-" ); String[] args = { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "cn=Directory Manager", "-w", "password", "-f", path }; assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0); // Read the entry back to get its history operational attribute. DN dn = DN.decode("uid=user.1,o=test"); Entry entry = DirectoryServer.getEntry(dn); // Check that encoding and decoding preserves the history information. Historical hist = Historical.load(entry); Attribute after = hist.encode(); List<Attribute> attrs = entry.getAttribute(Historical.historicalAttrType); Attribute before = attrs.get(0); assertEquals(after, before); } } opends/tests/unit-tests-testng/src/server/org/opends/server/synchronization/plugin/ModifyConflictTest.java
@@ -45,9 +45,6 @@ import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.synchronization.SynchronizationTestCase; import org.opends.server.synchronization.common.ChangeNumber; import org.opends.server.synchronization.plugin.FakeOperation; import org.opends.server.synchronization.plugin.FakeOperationComparator; import org.opends.server.synchronization.plugin.Historical; import org.opends.server.synchronization.protocol.ModifyContext; import org.opends.server.synchronization.protocol.SynchronizationMessage; import org.opends.server.synchronization.protocol.UpdateMessage; @@ -132,7 +129,7 @@ * sure...) */ testModify(entry, hist, "description", ModificationType.ADD, "older value", 2, false);; "older value", 2, false); /* * Now simulate an add at a later date that the previous replace. @@ -387,7 +384,7 @@ InternalClientConnection.getRootConnection(); ChangeNumber t = new ChangeNumber(date, (short) 0, (short) 0); /* create AttributeType description that will be usedfor this test */ /* create AttributeType description that will be used for this test */ AttributeType attrType = DirectoryServer.getAttributeType(attrName, true); @@ -405,7 +402,7 @@ modOp.setAttachment(SYNCHROCONTEXT, ctx); hist.replayOperation(modOp, entry); if (modType.intValue() == ModificationType.ADD.intValue()) if (modType == ModificationType.ADD) { AddOperation addOp = new AddOperation(connection, 1, 1, null, entry .getDN(), entry.getObjectClasses(), entry.getUserAttributes(),