Skip to content

Commit

Permalink
repo-sqale: added ContainerValueIdGenerator + unique CID test
Browse files Browse the repository at this point in the history
  • Loading branch information
virgo47 committed Mar 9, 2021
1 parent 0c5a958 commit c44bb9e
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 3 deletions.
@@ -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<Long> usedIds = new HashSet<>();
private final List<PrismContainerValue<?>> 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);
}
}
}
Expand Up @@ -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;
Expand All @@ -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<S extends ObjectType, Q extends QObject<R>, R extends MObject> {

private final PrismObject<S> object;
Expand Down Expand Up @@ -58,6 +69,8 @@ public String execute(SqaleTransformerContext transformerContext)
transformer = (ObjectSqlTransformer<S, Q, R>)
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()) {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -133,4 +149,5 @@ private void handlePostgresException(PSQLException psqlException)
}
}
}

}
Expand Up @@ -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
Expand Down
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<MUser> 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<MContainer> c = aliasFor(QContainer.CLASS);
List<MContainer> 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<MUser> 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!");
}
}
Expand Up @@ -85,7 +85,7 @@ private List<PrismContainer<?>> listAllPrismContainers(Visitable<?> object) {
return;
}

values.add((PrismContainer<?>) visitable);
values.add(container);
});

return values;
Expand Down

0 comments on commit c44bb9e

Please sign in to comment.