/*
* Copyright (c) 2010-2017 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.wf.impl.processors.primary.policy;
import com.evolveum.midpoint.model.api.context.EvaluatedPolicyRule;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.util.CloneUtil;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.util.MiscUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.wf.impl.processes.itemApproval.*;
import com.evolveum.midpoint.wf.impl.processors.primary.ModelInvocationContext;
import com.evolveum.midpoint.wf.impl.processors.primary.aspect.BasePrimaryChangeAspect;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import org.apache.commons.lang.BooleanUtils;
import org.jetbrains.annotations.NotNull;
import javax.xml.namespace.QName;
import java.util.*;
import static java.util.Comparator.naturalOrder;
/**
* @author mederly
*/
class ApprovalSchemaBuilder {
class Result {
@NotNull final ApprovalSchemaType schemaType;
@NotNull final SchemaAttachedPolicyRulesType attachedRules;
public Result(@NotNull ApprovalSchemaType schemaType,
@NotNull SchemaAttachedPolicyRulesType attachedRules) {
this.schemaType = schemaType;
this.attachedRules = attachedRules;
}
}
private class Fragment {
// object to which relations (approved, owner) are resolved
// TODO test this thoroughly in presence of non-direct rules and merged schemas
final PrismObject<?> target;
@NotNull final ApprovalSchemaType schema;
final EvaluatedPolicyRule policyRule;
final ApprovalCompositionStrategyType compositionStrategy;
private Fragment(ApprovalCompositionStrategyType compositionStrategy, PrismObject<?> target,
@NotNull ApprovalSchemaType schema, EvaluatedPolicyRule policyRule) {
this.compositionStrategy = compositionStrategy;
this.target = target;
this.schema = schema;
this.policyRule = policyRule;
}
private boolean isMergeableWith(Fragment other) {
return compositionStrategy != null && BooleanUtils.isTrue(compositionStrategy.isMergeable())
&& other.compositionStrategy != null && BooleanUtils.isTrue(other.compositionStrategy.isMergeable())
&& compositionStrategy.getOrder() != null && compositionStrategy.getOrder().equals(other.compositionStrategy.getOrder());
}
public boolean isExclusive() {
return compositionStrategy != null && BooleanUtils.isTrue(compositionStrategy.isExclusive());
}
public Integer getOrder() {
return compositionStrategy != null ? compositionStrategy.getOrder() : null;
}
}
private final List<Fragment> predefinedFragments = new ArrayList<>();
private final List<Fragment> standardFragments = new ArrayList<>();
private final List<Fragment> addOnFragments = new ArrayList<>(); // fragments to be merged into other ones
private final Set<Integer> exclusiveOrders = new HashSet<>();
private final Set<Integer> nonExclusiveOrders = new HashSet<>();
@NotNull private final BasePrimaryChangeAspect primaryChangeAspect;
@NotNull private final ApprovalSchemaHelper approvalSchemaHelper;
ApprovalSchemaBuilder(@NotNull BasePrimaryChangeAspect primaryChangeAspect, ApprovalSchemaHelper approvalSchemaHelper) {
this.primaryChangeAspect = primaryChangeAspect;
this.approvalSchemaHelper = approvalSchemaHelper;
}
// TODO target
void add(ApprovalSchemaType schema, ApprovalCompositionStrategyType compositionStrategy, PrismObject<?> defaultTarget,
EvaluatedPolicyRule policyRule) throws SchemaException {
Fragment fragment = new Fragment(compositionStrategy, defaultTarget, schema, policyRule);
if (isAddOnFragment(compositionStrategy)) {
if (compositionStrategy.getOrder() != null) {
throw new SchemaException("Both order and mergeIntoOrder/mergeIntoAll are set for " + schema);
}
addOnFragments.add(fragment);
} else {
standardFragments.add(fragment);
}
}
private boolean isAddOnFragment(ApprovalCompositionStrategyType cs) {
return cs != null && (!cs.getMergeIntoOrder().isEmpty() || BooleanUtils.isTrue(cs.isMergeIntoAll()));
}
// checks the existence of approvers beforehand, because we don't want to have an empty stage
boolean addPredefined(PrismObject<?> targetObject, @NotNull QName relationName, OperationResult result) {
RelationResolver resolver = primaryChangeAspect.createRelationResolver(targetObject, result);
List<ObjectReferenceType> approvers = resolver.getApprovers(Collections.singletonList(relationName));
if (!approvers.isEmpty()) {
ApprovalStageDefinitionType stageDef = new ApprovalStageDefinitionType();
stageDef.getApproverRef().addAll(approvers);
addPredefined(targetObject, stageDef);
return true;
} else {
return false;
}
}
void addPredefined(PrismObject<?> targetObject, ApprovalStageDefinitionType stageDef) {
ApprovalSchemaType schema = new ApprovalSchemaType();
schema.getStage().add(stageDef);
addPredefined(targetObject, schema);
}
void addPredefined(PrismObject<?> targetObject, ApprovalSchemaType schema) {
predefinedFragments.add(new Fragment(null, targetObject, schema, null));
}
Result buildSchema(ModelInvocationContext ctx, OperationResult result) throws SchemaException {
sortFragments(predefinedFragments);
sortFragments(standardFragments);
List<Fragment> allFragments = new ArrayList<>();
allFragments.addAll(predefinedFragments);
allFragments.addAll(standardFragments);
ApprovalSchemaType schemaType = new ApprovalSchemaType(ctx.prismContext);
SchemaAttachedPolicyRulesType attachedRules = new SchemaAttachedPolicyRulesType();
int i = 0;
while(i < allFragments.size()) {
List<Fragment> fragmentMergeGroup = getMergeGroup(allFragments, i);
i += fragmentMergeGroup.size();
checkExclusivity(fragmentMergeGroup);
processFragmentGroup(fragmentMergeGroup, schemaType, attachedRules, ctx, result);
}
return new Result(schemaType, attachedRules);
}
private void checkExclusivity(List<Fragment> fragmentMergeGroup) {
boolean isExclusive = fragmentMergeGroup.stream().anyMatch(f -> f.isExclusive());
Integer order = fragmentMergeGroup.get(0).getOrder();
if (isExclusive) {
if (exclusiveOrders.contains(order) || nonExclusiveOrders.contains(order)) {
throw new IllegalStateException("Exclusivity violation for schema fragments with the order of " + order);
}
exclusiveOrders.add(order);
} else {
if (exclusiveOrders.contains(order)) {
throw new IllegalStateException("Exclusivity violation for schema fragments with the order of " + order);
}
nonExclusiveOrders.add(order);
}
}
private List<Fragment> getMergeGroup(List<Fragment> fragments, int i) {
int j = i+1;
while (j < fragments.size() && fragments.get(i).isMergeableWith(fragments.get(j))) {
j++;
}
return new ArrayList<>(fragments.subList(i, j)); // result should be modifiable independently on the master list
}
private void processFragmentGroup(List<Fragment> fragments, ApprovalSchemaType resultingSchemaType,
SchemaAttachedPolicyRulesType attachedRules, ModelInvocationContext ctx, OperationResult result)
throws SchemaException {
Fragment firstFragment = fragments.get(0);
appendAddOnFragments(fragments);
List<ApprovalStageDefinitionType> fragmentStageDefs = cloneAndMergeStages(fragments);
if (fragmentStageDefs.isEmpty()) {
return; // probably shouldn't occur
}
fragmentStageDefs.sort(Comparator.comparing(s -> getNumber(s), Comparator.nullsLast(naturalOrder())));
RelationResolver relationResolver = primaryChangeAspect.createRelationResolver(firstFragment.target, result);
ReferenceResolver referenceResolver = primaryChangeAspect.createReferenceResolver(ctx.modelContext, ctx.taskFromModel, result);
int from = getStages(resultingSchemaType).size() + 1;
int i = from;
for (ApprovalStageDefinitionType stageDef : fragmentStageDefs) {
stageDef.setOrder(null);
stageDef.setNumber(i++);
approvalSchemaHelper.prepareStage(stageDef, relationResolver, referenceResolver);
resultingSchemaType.getStage().add(stageDef);
}
if (firstFragment.policyRule != null) {
SchemaAttachedPolicyRuleType attachedRule = new SchemaAttachedPolicyRuleType();
attachedRule.setStageMin(from);
attachedRule.setStageMax(i - 1);
attachedRule.setRule(firstFragment.policyRule.toEvaluatedPolicyRuleType());
attachedRules.getEntry().add(attachedRule);
}
}
private Integer getNumber(ApprovalStageDefinitionType s) {
return s.getNumber() != null ? s.getNumber() : s.getOrder();
}
private void appendAddOnFragments(List<Fragment> fragments) {
Integer order = fragments.get(0).getOrder();
if (order == null) {
return;
}
for (Fragment addOnFragment : addOnFragments) {
ApprovalCompositionStrategyType cs = addOnFragment.compositionStrategy;
if (BooleanUtils.isTrue(cs.isMergeIntoAll()) || cs.getMergeIntoOrder().contains(order)) {
fragments.add(addOnFragment);
}
}
}
private List<ApprovalStageDefinitionType> getStages(ApprovalSchemaType schema) {
return !schema.getStage().isEmpty() ? schema.getStage() : schema.getLevel();
}
private List<ApprovalStageDefinitionType> cloneAndMergeStages(List<Fragment> fragments) throws SchemaException {
if (fragments.size() == 1) {
return CloneUtil.cloneCollectionMembers(getStages(fragments.get(0).schema));
}
PrismContext prismContext = primaryChangeAspect.getChangeProcessor().getPrismContext();
ApprovalStageDefinitionType resultingStageDef = new ApprovalStageDefinitionType(prismContext);
fragments.sort((f1, f2) ->
Comparator.nullsLast(Comparator.<Integer>naturalOrder())
.compare(f1.compositionStrategy.getMergePriority(), f2.compositionStrategy.getMergePriority()));
for (Fragment fragment : fragments) {
mergeStageDefFromFragment(resultingStageDef, fragment);
}
return Collections.singletonList(resultingStageDef);
}
private void mergeStageDefFromFragment(ApprovalStageDefinitionType resultingStageDef, Fragment fragment) throws SchemaException {
List<ApprovalStageDefinitionType> stages = getStages(fragment.schema);
if (stages.size() != 1) {
throw new IllegalStateException("Couldn't merge approval schema fragment with stage count of not 1: " + fragment.schema);
}
ApprovalStageDefinitionType stageDefToMerge = stages.get(0);
List<QName> overwriteItems = fragment.compositionStrategy.getMergeOverwriting();
resultingStageDef.asPrismContainerValue().mergeContent(stageDefToMerge.asPrismContainerValue(), overwriteItems);
}
private void sortFragments(List<Fragment> fragments) {
fragments.forEach(f -> {
if (f.compositionStrategy != null && BooleanUtils.isTrue(f.compositionStrategy.isMergeable())
&& f.compositionStrategy.getOrder() == null) {
throw new IllegalStateException("Mergeable composition strategy with no order: "
+ f.compositionStrategy + " in " + f.policyRule);
}
});
// relying on the fact that the sort algorithm is stable
fragments.sort((f1, f2) -> {
ApprovalCompositionStrategyType s1 = f1.compositionStrategy;
ApprovalCompositionStrategyType s2 = f2.compositionStrategy;
Integer o1 = s1 != null ? s1.getOrder() : null;
Integer o2 = s2 != null ? s2.getOrder() : null;
if (o1 == null || o2 == null) {
return MiscUtil.compareNullLast(o1, o2);
}
int c = Integer.compare(o1, o2);
if (c != 0) {
return c;
}
// non-mergeable first
boolean m1 = BooleanUtils.isTrue(s1.isMergeable());
boolean m2 = BooleanUtils.isTrue(s2.isMergeable());
if (m1 && !m2) {
return 1;
} else if (!m1 && m2) {
return -1;
} else {
return 0;
}
});
}
}