diff --git a/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/ContainerValueIdGenerator.java b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/ContainerValueIdGenerator.java new file mode 100644 index 00000000000..953f08900e8 --- /dev/null +++ b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/ContainerValueIdGenerator.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010-2021 Evolveum and contributors + * + * This work is dual-licensed under the Apache License 2.0 + * and European Union Public License. See LICENSE file for details. + */ +package com.evolveum.midpoint.repo.sqale; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; + +import com.evolveum.midpoint.prism.PrismContainer; +import com.evolveum.midpoint.prism.PrismContainerDefinition; +import com.evolveum.midpoint.prism.PrismContainerValue; +import com.evolveum.midpoint.prism.PrismObject; +import com.evolveum.midpoint.util.exception.SchemaException; + +/** + * Generator assigning missing IDs to PCVs of multi-value containers. + */ +public class ContainerValueIdGenerator { + + private final PrismObject object; + private final Set usedIds = new HashSet<>(); + private final List> pcvsWithoutId = new ArrayList<>(); + + private long maxUsedId = 0; // tracks max CID (set to duplicate CID if found) + + public ContainerValueIdGenerator(@NotNull PrismObject object) { + this.object = object; + } + + /** Method inserts IDs for prism container values without IDs and returns highest CID. */ + public long generate() throws SchemaException { + processContainers(); + generateContainerIds(); + return maxUsedId; + } + + private void processContainers() throws SchemaException { + try { + //noinspection unchecked + object.accept(visitable -> { + if (!(visitable instanceof PrismContainer)) { + return; + } + + if (visitable instanceof PrismObject) { + return; + } + + PrismContainer container = (PrismContainer) visitable; + PrismContainerDefinition def = container.getDefinition(); + if (def.isSingleValue()) { + return; + } + + processContainer(container); + }); + } catch (DuplicateContainerIdException e) { + throw new SchemaException("CID " + maxUsedId + " is used repeatedly in the object!"); + } + } + + private void processContainer(PrismContainer container) { + for (PrismContainerValue val : container.getValues()) { + if (val.getId() != null) { + Long cid = val.getId(); + if (!usedIds.add(cid)) { + maxUsedId = cid; + throw DuplicateContainerIdException.INSTANCE; + } + maxUsedId = Math.max(maxUsedId, cid); + } else { + pcvsWithoutId.add(val); + } + } + } + + private void generateContainerIds() { + for (PrismContainerValue val : pcvsWithoutId) { + val.setId(nextId()); + } + } + + public long nextId() { + maxUsedId++; + return maxUsedId; + } + + private static class DuplicateContainerIdException extends RuntimeException { + static final DuplicateContainerIdException INSTANCE = new DuplicateContainerIdException(); + + private DuplicateContainerIdException() { + super(null, null, false, false); + } + } +} diff --git a/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/operations/AddObjectOperation.java b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/operations/AddObjectOperation.java index aca391649ee..66237570a69 100644 --- a/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/operations/AddObjectOperation.java +++ b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/operations/AddObjectOperation.java @@ -17,6 +17,7 @@ import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.repo.api.RepoAddOptions; +import com.evolveum.midpoint.repo.sqale.ContainerValueIdGenerator; import com.evolveum.midpoint.repo.sqale.SqaleTransformerContext; import com.evolveum.midpoint.repo.sqale.qmodel.SqaleTableMapping; import com.evolveum.midpoint.repo.sqale.qmodel.object.MObject; @@ -29,6 +30,16 @@ import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; +/* +TODO: implementation note: + Typically I'd use "technical" dependencies in a constructor and then the object/options/result + would be parameters of execute(). Unfortunately I don't know how to do that AND capture + the parametric types in the operations object. I could hide it behind this object and then capture + it in another "actual operation" object, but that does not make any sense. + That's why the creation is with actual parameters and execute() takes technical ones (possibly + some richer "context" object later to provide more dependencies if necessary). + The "context" could go to construction too, but than it would be all mixed too much. Sorry. +*/ public class AddObjectOperation, R extends MObject> { private final PrismObject object; @@ -58,6 +69,8 @@ public String execute(SqaleTransformerContext transformerContext) transformer = (ObjectSqlTransformer) rootMapping.createTransformer(transformerContext); + // we don't want CID generation here, because overwrite works different then normal add + if (object.getOid() == null) { return addObjectWithoutOid(); } else if (options.isOverwrite()) { @@ -80,10 +93,11 @@ private String overwriteObject() { } private String addObjectWithOid() throws SchemaException { + long lastCid = new ContainerValueIdGenerator(object).generate(); try (JdbcSession jdbcSession = sqlRepoContext.newJdbcSession().startTransaction()) { S schemaObject = object.asObjectable(); R row = transformer.toRowObjectWithoutFullObject(schemaObject, jdbcSession); - // TODO set row.containerIdSeq + row.containerIdSeq = lastCid + 1; transformer.storeRelatedEntities(row, schemaObject, jdbcSession); transformer.setFullObject(row, schemaObject); UUID oid = jdbcSession.newInsert(root) @@ -96,8 +110,10 @@ private String addObjectWithOid() throws SchemaException { } private String addObjectWithoutOid() throws SchemaException { + long lastCid = new ContainerValueIdGenerator(object).generate(); try (JdbcSession jdbcSession = sqlRepoContext.newJdbcSession().startTransaction()) { R row = transformer.toRowObjectWithoutFullObject(object.asObjectable(), jdbcSession); + row.containerIdSeq = lastCid + 1; // first insert without full object, because we don't know the OID yet UUID oid = jdbcSession.newInsert(root) // default populate mapper ignores null, that's good, especially for objectType @@ -133,4 +149,5 @@ private void handlePostgresException(PSQLException psqlException) } } } + } diff --git a/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/qmodel/object/MObject.java b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/qmodel/object/MObject.java index 6b4caba8e6f..b01943d46f7 100644 --- a/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/qmodel/object/MObject.java +++ b/repo/repo-sqale/src/main/java/com/evolveum/midpoint/repo/sqale/qmodel/object/MObject.java @@ -26,7 +26,7 @@ public class MObject { public Integer tenantRefTargetType; public Integer tenantRefRelationId; public String lifecycleState; - public Long containerIdSeq; + public Long containerIdSeq; // next available container ID (for PCV of multi-valued containers) public Integer version; public byte[] ext; // metadata diff --git a/repo/repo-sqale/src/test/java/com/evolveum/midpoint/repo/sqale/SqaleRepoAddObjectTest.java b/repo/repo-sqale/src/test/java/com/evolveum/midpoint/repo/sqale/SqaleRepoAddObjectTest.java index 14ee4fb3877..daaf82bd3fc 100644 --- a/repo/repo-sqale/src/test/java/com/evolveum/midpoint/repo/sqale/SqaleRepoAddObjectTest.java +++ b/repo/repo-sqale/src/test/java/com/evolveum/midpoint/repo/sqale/SqaleRepoAddObjectTest.java @@ -16,11 +16,17 @@ import org.testng.annotations.Test; +import com.evolveum.midpoint.repo.sqale.qmodel.common.MContainer; +import com.evolveum.midpoint.repo.sqale.qmodel.common.MContainerType; +import com.evolveum.midpoint.repo.sqale.qmodel.common.QContainer; import com.evolveum.midpoint.repo.sqale.qmodel.focus.MUser; import com.evolveum.midpoint.repo.sqale.qmodel.focus.QUser; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; import com.evolveum.midpoint.util.exception.SchemaException; +import com.evolveum.midpoint.xml.ns._public.common.common_3.AssignmentType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.OperationExecutionType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.RoleType; import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType; public class SqaleRepoAddObjectTest extends SqaleRepoBaseTest { @@ -146,4 +152,100 @@ public void test111AddSecondObjectWithTheSameOidThrowsObjectAlreadyExists() assertCount(QUser.class, baseCount); } + @Test + public void test200AddObjectWithMultivalueContainers() + throws ObjectAlreadyExistsException, SchemaException { + OperationResult result = createOperationResult(); + + given("user with assignment and ref"); + String userName = "user" + getTestNumber(); + String targetRef1 = UUID.randomUUID().toString(); + String targetRef2 = UUID.randomUUID().toString(); + UserType user = new UserType(prismContext) + .name(userName) + .assignment(new AssignmentType(prismContext) + .targetRef(targetRef1, RoleType.COMPLEX_TYPE)) + .assignment(new AssignmentType(prismContext) + .targetRef(targetRef2, RoleType.COMPLEX_TYPE)); + + when("adding it to the repository"); + repositoryService.addObject(user.asPrismObject(), null, result); + + then("object and its container rows are created and container IDs are assigned"); + assertResult(result); + + QUser u = aliasFor(QUser.class); + List users = select(u, u.nameOrig.eq(userName)); + assertThat(users).hasSize(1); + MUser userRow = users.get(0); + assertThat(userRow.oid).isNotNull(); + assertThat(userRow.containerIdSeq).isEqualTo(3); // next free container number + + QContainer c = aliasFor(QContainer.CLASS); + List containers = select(c, c.ownerOid.eq(userRow.oid)); + // TODO this fails ATM, not implemented yet + assertThat(containers).hasSize(2) + .allMatch(cRow -> cRow.ownerOid.equals(userRow.oid) + && cRow.containerType == MContainerType.ASSIGNMENT) + .extracting(cRow -> cRow.cid) + .containsExactlyInAnyOrder(1L, 2L); + } + + @Test + public void test201AddObjectWithMultivalueRefs() + throws ObjectAlreadyExistsException, SchemaException { + OperationResult result = createOperationResult(); + + given("user with ref"); + String userName = "user" + getTestNumber(); + String targetRef1 = UUID.randomUUID().toString(); + String targetRef2 = UUID.randomUUID().toString(); + UserType user = new UserType(prismContext) + .name(userName) + .linkRef(targetRef1, RoleType.COMPLEX_TYPE) + .linkRef(targetRef2, RoleType.COMPLEX_TYPE); + + when("adding it to the repository"); + repositoryService.addObject(user.asPrismObject(), null, result); + + then("object and its container rows are created and container IDs are assigned"); + assertResult(result); + + QUser u = aliasFor(QUser.class); + List users = select(u, u.nameOrig.eq(userName)); + assertThat(users).hasSize(1); + MUser userRow = users.get(0); + assertThat(userRow.oid).isNotNull(); + assertThat(userRow.containerIdSeq).isEqualTo(1); // cid sequence is in initial state + + // TODO assert ref rows + } + + @Test + public void test290DuplicateCidInsideOneContainerIsCaughtByPrism() { + expect("object construction with duplicate CID inside container fails immediately"); + assertThatThrownBy(() -> new UserType(prismContext) + .assignment(new AssignmentType() + .targetRef("ref1", RoleType.COMPLEX_TYPE).id(1L)) + .assignment(new AssignmentType() + .targetRef("ref2", RoleType.COMPLEX_TYPE).id(1L))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Attempt to add a container value with an id that already exists: 1"); + } + + @Test + public void test291DuplicateCidInDifferentContainersIsCaughtByRepo() { + OperationResult result = createOperationResult(); + + given("object with duplicate CID in different containers"); + UserType user = new UserType(prismContext) + .name("any name") + .assignment(new AssignmentType().id(1L)) + .operationExecution(new OperationExecutionType().id(1L)); + + expect("adding object to repository throws exception"); + assertThatThrownBy(() -> repositoryService.addObject(user.asPrismObject(), null, result)) + .isInstanceOf(SchemaException.class) + .hasMessage("CID 1 is used repeatedly in the object!"); + } } diff --git a/repo/repo-sql-impl/src/main/java/com/evolveum/midpoint/repo/sql/util/PrismIdentifierGenerator.java b/repo/repo-sql-impl/src/main/java/com/evolveum/midpoint/repo/sql/util/PrismIdentifierGenerator.java index 66d2894c037..130b7e0667e 100644 --- a/repo/repo-sql-impl/src/main/java/com/evolveum/midpoint/repo/sql/util/PrismIdentifierGenerator.java +++ b/repo/repo-sql-impl/src/main/java/com/evolveum/midpoint/repo/sql/util/PrismIdentifierGenerator.java @@ -85,7 +85,7 @@ private List> listAllPrismContainers(Visitable object) { return; } - values.add((PrismContainer) visitable); + values.add(container); }); return values;