| | |
| | | /** The set of objectclasses for this entry. */ |
| | | private Map<ObjectClass,String> objectClasses; |
| | | |
| | | /** |
| | | * The set of objectclasses of this entry in its attribute form, built on demand by |
| | | * {@link #getObjectClassAttribute()}. Every change of {@link #objectClasses} must reset it: |
| | | * a backend stores the objectclasses themselves but indexes the entry through this attribute, |
| | | * so an attribute left behind by a change takes the objectClass index out of step with the |
| | | * stored entry, without anything failing. |
| | | */ |
| | | private Attribute objectClassAttribute; |
| | | |
| | | /** The DN for this entry. */ |
| | |
| | | } |
| | | |
| | | objectClasses.put(oc, oc.getNameOrOID()); |
| | | objectClassAttribute = null; |
| | | } |
| | | |
| | | |
| | |
| | | if (attributeType.isObjectClass()) |
| | | { |
| | | objectClasses.clear(); |
| | | objectClassAttribute = null; |
| | | return true; |
| | | } |
| | | return userAttributes.remove(attributeType) != null |
| | |
| | | if (attribute.isEmpty()) |
| | | { |
| | | objectClasses.clear(); |
| | | objectClassAttribute = null; |
| | | return true; |
| | | } |
| | | |
| | |
| | | if (oc.hasNameOrOID(ocName)) |
| | | { |
| | | objectClasses.remove(oc); |
| | | objectClassAttribute = null; |
| | | matchFound = true; |
| | | break; |
| | | } |
| | |
| | | AttributeType attrType = attribute.getAttributeDescription().getAttributeType(); |
| | | // We will not do any validation of the object classes - this is |
| | | // left to the caller. |
| | | objectClassAttribute = null; |
| | | if (replace) |
| | | { |
| | | objectClasses.clear(); |
| | |
| | | import org.opends.server.protocols.internal.InternalClientConnection; |
| | | import org.opends.server.protocols.internal.InternalSearchOperation; |
| | | import org.opends.server.protocols.internal.SearchRequest; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.BackupConfig; |
| | | import org.opends.server.types.BackupDirectory; |
| | | import org.opends.server.types.DirectoryException; |
| | |
| | | |
| | | private Map<String, IndexType[]> backendIndexes = new HashMap<>(); |
| | | { |
| | | backendIndexes.put("objectClass", new IndexType[] { IndexType.EQUALITY }); |
| | | backendIndexes.put("entryUUID", new IndexType[] { IndexType.EQUALITY }); |
| | | backendIndexes.put("cn", new IndexType[] { IndexType.SUBSTRING }); |
| | | backendIndexes.put("sn", new IndexType[] { IndexType.PRESENCE, IndexType.EQUALITY, IndexType.SUBSTRING }); |
| | |
| | | assertThat((Object) returnedEntries.get(0).getName()).isEqualTo(newEntry.getName()); |
| | | } |
| | | |
| | | /** |
| | | * A modification of the object classes of an entry must reach the objectClass index even when |
| | | * the objectClass attribute of the modified entry has already been read - which the conflict |
| | | * resolution of a replayed modification does with every delete of an object class value, before |
| | | * the core applies the modifications to the entry. |
| | | */ |
| | | @Test |
| | | public void testModifyObjectClassOfAnAlreadyReadEntry() throws Exception |
| | | { |
| | | final AttributeType objectClassType = CoreSchema.getObjectClassAttributeType(); |
| | | final Entry oldEntry = TestCaseUtils.makeEntry( |
| | | "dn: uid=user.13,ou=People," + testBaseDN, |
| | | "objectClass: top", |
| | | "objectClass: person", |
| | | "objectClass: organizationalPerson", |
| | | "objectClass: inetOrgPerson", |
| | | "givenName: Abbey", |
| | | "sn: Abbie", |
| | | "cn: Abbey Abbie", |
| | | "uid: user.13"); |
| | | backend.addEntry(oldEntry, mock(AddOperation.class)); |
| | | try |
| | | { |
| | | final Entry newEntry = oldEntry.duplicate(false); |
| | | // The read of the entry done by the conflict resolution of a replayed modification. |
| | | newEntry.hasValue(AttributeDescription.create(objectClassType), ByteString.valueOfUtf8("person")); |
| | | |
| | | final Attribute addedClass = create(objectClassType, "extensibleObject"); |
| | | final Attribute deletedClass = create(objectClassType, "organizationalPerson"); |
| | | // The core applies the modifications with addAttribute() and removeAttribute(). |
| | | newEntry.addAttribute(addedClass, new LinkedList<ByteString>()); |
| | | newEntry.removeAttribute(deletedClass, new LinkedList<ByteString>()); |
| | | |
| | | ModifyOperation modifyOp = mock(ModifyOperation.class); |
| | | when(modifyOp.getModifications()).thenReturn( |
| | | Arrays.asList(new Modification(ADD, addedClass), new Modification(DELETE, deletedClass))); |
| | | backend.replaceEntry(oldEntry, newEntry, modifyOp); |
| | | |
| | | // The added object class must be searchable through the index, |
| | | final List<Entry> returnedEntries = new ArrayList<>(); |
| | | backend.search(createSearchOperation( |
| | | testBaseDN, SearchScope.WHOLE_SUBTREE, "(objectClass=extensibleObject)", returnedEntries)); |
| | | assertThat(returnedEntries).hasSize(1); |
| | | assertThat((Object) returnedEntries.get(0).getName()).isEqualTo(newEntry.getName()); |
| | | |
| | | // and the deleted one must be gone from it. |
| | | assertThat(verifyObjectClassIndex()).isEqualTo(0); |
| | | } |
| | | finally |
| | | { |
| | | backend.deleteEntry(oldEntry.getName(), mock(DeleteOperation.class)); |
| | | } |
| | | } |
| | | |
| | | /** Returns the number of both the missing and the stale records of the objectClass index. */ |
| | | private long verifyObjectClassIndex() throws Exception |
| | | { |
| | | VerifyConfig completeness = new VerifyConfig(); |
| | | completeness.setBaseDN(testBaseDN); |
| | | completeness.addCompleteIndex("objectClass"); |
| | | |
| | | VerifyConfig cleanliness = new VerifyConfig(); |
| | | cleanliness.setBaseDN(testBaseDN); |
| | | cleanliness.addCleanIndex("objectClass"); |
| | | |
| | | return backend.verifyBackend(completeness) + backend.verifyBackend(cleanliness); |
| | | } |
| | | |
| | | private SearchOperation createSearchOperation(DN baseDN, SearchScope scope, String searchFilter, |
| | | final List<Entry> returnedEntries) throws DirectoryException |
| | | { |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.plugin; |
| | | |
| | |
| | | public class ModifyConflictTest extends ReplicationTestCase |
| | | { |
| | | private static final String ORGANIZATION = "organization"; |
| | | private static final String OBJECTCLASS = "objectClass"; |
| | | private static final String EXTENSIBLEOBJECT = "extensibleObject"; |
| | | private static final String DISPLAYNAME = "displayName"; |
| | | private static final String EMPLOYEENUMBER = "employeeNumber"; |
| | | private static final String DESCRIPTION = "description"; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Test that a replayed modification of the objectClass attribute leaves the entry handing out |
| | | * the object classes it ends up with. |
| | | * <p> |
| | | * Solving the conflicts of a delete reads the entry, and it does so before the core applies the |
| | | * modifications to it. The read must not fix what the entry answers afterwards: the backend |
| | | * stores the object classes of the entry but indexes it through its objectClass attribute, so |
| | | * an attribute left behind by the modification silently takes the objectClass index out of step |
| | | * with the stored entry. |
| | | */ |
| | | @Test |
| | | public void replayObjectClassAddAndDelete() throws Exception |
| | | { |
| | | Entry entry = initializeEntry(); |
| | | EntryHistorical hist = EntryHistorical.newInstanceFromEntry(entry); |
| | | |
| | | List<Modification> mods = newArrayList( |
| | | newModification(ADD, OBJECTCLASS, EXTENSIBLEOBJECT), |
| | | newModification(DELETE, OBJECTCLASS, ORGANIZATION)); |
| | | |
| | | // The conflict resolution, which reads the entry to solve the delete. |
| | | replayModifies(entry, hist, 10, mods); |
| | | assertThat(mods).hasSize(2); |
| | | |
| | | // The core, which applies the modifications to the entry. |
| | | applyModificationsTheWayTheCoreDoes(entry, mods); |
| | | |
| | | assertThat(entry.getObjectClasses().values()).containsOnly(EXTENSIBLEOBJECT); |
| | | assertThat(objectClassAttributeOf(entry)).containsOnly(EXTENSIBLEOBJECT); |
| | | } |
| | | |
| | | /** |
| | | * Applies the modifications to the entry the way {@code LocalBackendModifyOperation} does, that |
| | | * is with {@code addAttribute()} and {@code removeAttribute()} rather than with |
| | | * {@code Entry.applyModifications()}. |
| | | */ |
| | | private void applyModificationsTheWayTheCoreDoes(Entry entry, List<Modification> mods) |
| | | { |
| | | for (Modification mod : mods) |
| | | { |
| | | Attribute attr = mod.getAttribute(); |
| | | switch (mod.getModificationType().asEnum()) |
| | | { |
| | | case ADD: |
| | | entry.addAttribute(attr, new LinkedList<ByteString>()); |
| | | break; |
| | | case DELETE: |
| | | entry.removeAttribute(attr, new LinkedList<ByteString>()); |
| | | break; |
| | | default: |
| | | entry.replaceAttribute(attr); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** Returns the object classes of the entry as its objectClass attribute reports them. */ |
| | | private List<String> objectClassAttributeOf(Entry entry) |
| | | { |
| | | List<String> values = new LinkedList<>(); |
| | | for (Attribute attr : entry.getAllAttributes(getObjectClassAttributeType())) |
| | | { |
| | | for (ByteString value : attr) |
| | | { |
| | | values.add(value.toString()); |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | /** |
| | | * Check that the mods given as first parameter match the next parameters. |
| | | * |
| | | * @param mods The mods that must be tested. |
| | |
| | | TestCaseUtils.startServer(); |
| | | } |
| | | |
| | | /** Returns an entry to delete object class values from. */ |
| | | /** Returns an entry to change the object classes of. */ |
| | | private Entry newTestUserEntry() throws Exception |
| | | { |
| | | return TestCaseUtils.makeEntry( |
| | |
| | | assertThat(e.getObjectClasses().values()).containsOnly("top", "person", "organizationalPerson"); |
| | | } |
| | | |
| | | /* |
| | | * An entry holds its object classes apart from its attributes, and hands them out as an |
| | | * attribute which it builds on demand and keeps. Every change of the object classes must be |
| | | * visible to the next read of that attribute: a backend stores the object classes themselves |
| | | * but indexes the entry through the attribute, so an attribute left behind by a change silently |
| | | * takes the object class index out of step with the stored entry. The tests below read the |
| | | * attribute first, the way the server reads an entry before modifying it. |
| | | */ |
| | | |
| | | /** Returns the object classes of the entry as its object class attribute reports them. */ |
| | | private List<String> objectClassAttributeOf(Entry e) |
| | | { |
| | | List<String> values = new ArrayList<>(); |
| | | for (Attribute attr : e.getAllAttributes(getObjectClassAttributeType())) |
| | | { |
| | | for (ByteString value : attr) |
| | | { |
| | | values.add(value.toString()); |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | /** An object class added with {@code addAttribute()}, the way the core applies "add: objectClass". */ |
| | | @Test |
| | | public void testObjectClassAttributeAfterAddAttribute() throws Exception |
| | | { |
| | | Entry e = newTestUserEntry(); |
| | | assertThat(objectClassAttributeOf(e)).doesNotContain("extensibleObject"); |
| | | |
| | | e.addAttribute(Attributes.create(getObjectClassAttributeType(), "extensibleObject"), |
| | | new LinkedList<ByteString>()); |
| | | |
| | | assertThat(objectClassAttributeOf(e)) |
| | | .containsOnly("top", "person", "organizationalPerson", "inetOrgPerson", "extensibleObject"); |
| | | } |
| | | |
| | | /** |
| | | * An object class removed with {@code removeAttribute()}, the way the core applies |
| | | * "delete: objectClass". |
| | | */ |
| | | @Test |
| | | public void testObjectClassAttributeAfterRemoveAttribute() throws Exception |
| | | { |
| | | Entry e = newTestUserEntry(); |
| | | assertThat(objectClassAttributeOf(e)).contains("organizationalPerson"); |
| | | |
| | | e.removeAttribute(Attributes.create(getObjectClassAttributeType(), "organizationalPerson"), |
| | | new LinkedList<ByteString>()); |
| | | |
| | | assertThat(objectClassAttributeOf(e)).containsOnly("top", "person", "inetOrgPerson"); |
| | | } |
| | | |
| | | /** |
| | | * The object classes replaced with {@code replaceAttribute()}, the way the core applies |
| | | * "replace: objectClass". |
| | | */ |
| | | @Test |
| | | public void testObjectClassAttributeAfterReplaceAttribute() throws Exception |
| | | { |
| | | Entry e = newTestUserEntry(); |
| | | assertThat(objectClassAttributeOf(e)).contains("inetOrgPerson"); |
| | | |
| | | e.replaceAttribute(Attributes.create(getObjectClassAttributeType(), "domain")); |
| | | |
| | | assertThat(objectClassAttributeOf(e)).containsOnly("domain"); |
| | | } |
| | | |
| | | /** An object class added with {@code addObjectClass()}. */ |
| | | @Test |
| | | public void testObjectClassAttributeAfterAddObjectClass() throws Exception |
| | | { |
| | | Entry e = newTestUserEntry(); |
| | | assertThat(objectClassAttributeOf(e)).doesNotContain("extensibleObject"); |
| | | |
| | | e.addObjectClass(CoreSchema.getExtensibleObjectObjectClass()); |
| | | |
| | | assertThat(objectClassAttributeOf(e)) |
| | | .containsOnly("top", "person", "organizationalPerson", "inetOrgPerson", "extensibleObject"); |
| | | } |
| | | |
| | | /** All the object classes removed with {@code removeAttribute()}, then a new one added. */ |
| | | @Test |
| | | public void testObjectClassAttributeAfterRemoveOfTheWholeAttribute() throws Exception |
| | | { |
| | | Entry e = newTestUserEntry(); |
| | | assertThat(objectClassAttributeOf(e)).contains("inetOrgPerson"); |
| | | |
| | | e.removeAttribute(getObjectClassAttributeType()); |
| | | assertThat(objectClassAttributeOf(e)).isEmpty(); |
| | | |
| | | e.addObjectClass(CoreSchema.getTopObjectClass()); |
| | | assertThat(objectClassAttributeOf(e)).containsOnly("top"); |
| | | } |
| | | |
| | | /** |
| | | * Test the {@link Entry#parseAttribute(String)} method. |
| | | */ |