/*
* Copyright (c) 2010-2016 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.model.impl.lens;
import javax.xml.namespace.QName;
import com.evolveum.midpoint.model.api.context.AssignmentPathSegment;
import com.evolveum.midpoint.model.api.context.EvaluationOrder;
import com.evolveum.midpoint.model.common.expression.ItemDeltaItem;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContainerDefinition;
import com.evolveum.midpoint.prism.PrismContainerValue;
import com.evolveum.midpoint.prism.xml.XsdTypeMapper;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Primary duty of this class is to be a part of assignment path. (This is what is visible through its interface,
* AssignmentPathSegment.) However, it also serves as a place where auxiliary information about assignment evaluation
* is stored.
*
* @author semancik
*
*/
@SuppressWarnings("WeakerAccess")
public class AssignmentPathSegmentImpl implements AssignmentPathSegment {
private static final Trace LOGGER = TraceManager.getTrace(AssignmentPathSegmentImpl.class);
// "assignment path segment" information
final ObjectType source; // we avoid "getter" notation for some final fields to simplify client code
private final ItemDeltaItem<PrismContainerValue<AssignmentType>,PrismContainerDefinition<AssignmentType>> assignmentIdi;
private final boolean isAssignment; // false means inducement
private QName relation;
private ObjectType target;
// assignment evaluation information
final String sourceDescription; // Human readable text describing the source (for error messages)
private boolean pathToSourceValid; // Is the whole path to *source* valid, i.e. enabled (meaning activation.effectiveStatus)?
private boolean validityOverride = false; // Should we evaluate content of the assignment even if it's not valid i.e. enabled?
// This is set to true on the first assignment in the chain.
// Last segment with the same evaluation order. Used for higher-order (summary-rewriting) inducements.
// See e.g. TestAssignmentProcessor2.test600.
private Integer lastEqualOrderSegmentIndex;
/**
* Assignments and inducements can carry constructions, focus mappings, and policy rules.
* We can call these "assignment/inducement payload", or "payload" for short.
*
* When looking at assignments/inducements in assignment path, payload of some assignments/inducements will be collected
* to focus, while payload from others will be not. How we know what to collect?
*
* For assignments/inducements belonging directly to the focus, we take payload from all the assignments. Not from inducements.
* For assignments/inducements belonging to roles (assigned to focus), we take payload from all the inducements of order 1.
* For assignments/inducements belonging to meta-roles (assigned to roles), we take payload from all the inducements of order 2.
* And so on. (It is in fact a bit more complicated, as described below when discussing relations. But OK for the moment.)
*
* To know whether to collect payload from assignment/inducement, i.e. from assignment path segment, we
* define "isMatchingOrder" attribute - and collect only if value of this attribute is true.
*
* How we compute this attribute?
*
* Each assignment path segment has an evaluation order. First assignment has an evaluation order of 1, second
* assignment has an order of 2, etc. Order of a segment can be seen as the number of assignments segments in the path
* (including itself). And, for "real" assignments (i.e. not inducements), we collect payload from assignment segments
* of order 1.
*
* But what about inducements? There are two - somewhat related - questions:
*
* 1. How we compute isMatchingOrder for inducement segments?
* 2. How we compute evaluation order for inducement segments?
*
* As for #1: To compute isMatchingOrder, we must take evaluation order of the _previous_ segment, and compare
* it with the order (or, more generally, order constraints) of the inducement. If they match, we say that inducement
* has matching order.
*
* As for #2: It is not usual that inducements have targets (roles) with another assignments, i.e. that evaluation continues
* after an inducement segment. But it definitely could happen. We can look at it this way: inducement is something like
* a "shortcut" that creates an assignment where no assignment was before. E.g. if we have R1 -A-> MR1 -I-> MR2,
* the "newly created" assignment is R1 -A-> MR2. I.e. as if the " -A-> MR1 -I-> " part was just replaced by " -A-> ".
* If the inducement is of higher order, even more assignments are "cut out". From R1 -A-> MR1 -A-> MMR1 -(I2)-> MR2
* we have R1 -A-> MR2, i.e. we cut out " -A-> MR1 -A-> MMR1 -(I2)-> " and replaced it by " -A-> ".
* So it looks like that when computing new evaluation order of an inducement, we have to "go back" few steps
* through the assignment path.
*
* Such situation can also easily occur when org structures are used. As depicted in TestAssignmentProcessor2.test500
* and later, imagine this:
*
* Org1 -----I----+ Org2 -----I----+
* ^ | (orderConstraints 1..N) ^ | (orderConstraints: manager: 1)
* | | | |
* | V | V
* Org11 Admin Org21 Admin
* ^ ^
* | (manager)
* | |
* jack jack
*
* In order to resolve such cases, we created the "resetOrder" attribute. It can be applied either to
* summary order, or to one or more specific relations (please, don't specify it for both summary order
* and specific relations!)
*
* In the above examples, we could specify e.g. resetOrder=1 for summary order (for both left and right situation).
* For the right one, we could instead specify resetOrder=0 for org:manager relation; although the former solution
* is preferable.
*
* By simply specifying inducement order greater than 1 (e.g. 5) without any specific order constraints,
* we implicitly provide resetOrder instruction for summary order that points order-1 levels back (i.e. 4 levels back
* for order=5).
*
* Generally, it is preferred to use resetOrder for summary order. It works well with both normal and target
* evaluation order. When resetting individual components, target evaluation order can have problems, as shown
* in TestEvaluationProcessor2.test520.
*
* ----
*
* Because evaluation order can "increase" and "decrease", it is possible that it goes to zero or below, and then
* increase back to positive numbers. Is that OK? Imagine this:
*
* (Quite an ugly example, but such things might exist.)
*
* Metarole:CrewMember ----I----+ Metarole:Sailors
* A | A
* | | |
* | V |
* Pirate Sailor
* A
* |
* |
* jack
*
* When evaluating jack->Pirate assignment, it is OK to collect from everything (Pirate, CrewMember, Sailor, Sailors).
*
* But when evaluating Pirate as a focal object (forget about jack for the moment), we have Pirate->CrewMember assignment.
* For this assignment we should ignore payload from Sailor (obviously, because the order is not matching), but from
* Metarole:Sailors as well. Payload from Sailors is not connected to Pirate in any meaningful way. (For example, if
* Sailors prescribes an account construction for Sailor, it is of no use to collect this construction when evaluating
* Pirate as focal object!)
*
* Evaluating relations
* ====================
*
* With the arrival of various kinds of relations (deputy, manager, approver, owner) things got a bit complicated.
* For instance, the deputy relation cannot be used to determine evaluation order in a usual way, because if
* we have this situation:
*
* Pirate -----I-----> Sailor
* A
* | (default)
* |
* jack
* A
* | (deputy)
* |
* barbossa
*
* we obviously want to have barbossa to obtain all payload from roles Pirate and Sailor: exactly as jack does.
* So, the evaluation order of " barbossa -A-> jack -A-> Pirate " should be 1, not two. So deputy is a very special
* kind of relation, that does _not_ increase the traditional evaluation order. But we really want to record
* the fact that the deputy is on the assignment path; therefore, besides traditional "scalar" evaluation order
* (called "summaryOrder") we maintain evaluation orders for each relation separately. In the above example,
* the evaluation orders would be:
* barbossa--->jack summary: 0, deputy: 1, default: 0
* jack--->Pirate summary: 1, deputy: 1, default: 1
* Pirate-I->Sailor summary: 1, deputy: 1, default: 1 (because the inducement has a default order of 1)
*
* When we determine matchingOrder for an inducement (question #1 above), we can ask about summary order,
* as well as about individual components (deputy and default, in this case).
*
* Actually, we have three categories of relations (see MID-3581):
*
* - membership relations: apply membership references, payload, authorizations, gui config
* - delegation relations: similar to membership, bud different handling of order
* - other relations: apply membership references but in limited way; payload, authorizations and gui config
* are - by default - not applied.
*
* Currently, membership relations are: default (i.e. member), manager, and meta.
* Delegation: deputy.
* Other: approver, owner, and custom ones.
*
* As for the "other" relations: they bring membership information, but limited to the target that is directly
* assigned. So if jack is an approver for role Landluber, his roleMembershipRef will contain ref to Landluber
* but not e.g. to role Earthworm that is induced by Landluber. In a similar way, payload from targets assigned
* by "other" relations is not collected.
*
* Both of this can be overridden by using specific orderConstraints on particular inducement.
* Set of order constraint is considered to match evaluation order with "other" relations, if for each such "other"
* relation it contains related constraint. So, if one explicitly wants an inducement to be applied when
* "approver" relation is encountered, he may do so.
*
* Note: authorizations and gui config information are not considered to be a payload, because they are not
* part of an assignment/inducement - they are part of a role. In the future we might move them into
* assignments/inducements.
*
* Collecting target policy rules
* ==============================
*
* Special consideration must be given when collecting target policy rules, i.e. rules that are attached to
* assignment targets. Such rules are typically attached to roles that are being assigned. So let's consider this:
*
* rule1 (e.g. assignment approval policy rule)
* A
* |
* |
* Pirate
* A
* |
* |
* jack
*
* When evaluating jack->Pirate assignment, rule1 would not be normally taken into account, because its assignment
* (Pirate->rule1) has an order of 2. However, we want to collect it - but not as an item related to focus, but
* as an item related to evaluated assignment's target. Therefore besides isMatchingOrder we maintain isMatchingOrderForTarget
* that marks all segments (assignments/inducements) that contain policy rules relevant to the evaluated assignment's target.
*
* The computation is done by maintaining two evaluationOrders:
* - plain evaluationOrder that is related to the focus object [starts from ZERO.advance(first assignment relation)]
* - special evaluationOrderForTarget that is related to the target of the assignment being evaluated [starts from ZERO]
*
* Finally, how we distinguish between "this target" and "other targets" policy rules? Current approach
* is like this: count the number of times the evaluation order for target reached ZERO level. First encounter with
* that level is on "this target". And we assume that each subsequent marks a target that is among "others".
*
* @see AssignmentEvaluator#appliesDirectly
*/
private Boolean isMatchingOrder = null;
private EvaluationOrder evaluationOrder;
private Boolean isMatchingOrderForTarget = null;
private EvaluationOrder evaluationOrderForTarget;
private boolean processMembership = false;
private ObjectType varThisObject;
AssignmentPathSegmentImpl(ObjectType source, String sourceDescription,
ItemDeltaItem<PrismContainerValue<AssignmentType>, PrismContainerDefinition<AssignmentType>> assignmentIdi,
boolean isAssignment) {
this.source = source;
this.sourceDescription = sourceDescription;
this.assignmentIdi = assignmentIdi;
this.isAssignment = isAssignment;
}
@Override
public boolean isAssignment() {
return isAssignment;
}
public ItemDeltaItem<PrismContainerValue<AssignmentType>,PrismContainerDefinition<AssignmentType>> getAssignmentIdi() {
return assignmentIdi;
}
@Override
public AssignmentType getAssignment() {
if (assignmentIdi == null || assignmentIdi.getItemNew() == null || assignmentIdi.getItemNew().isEmpty()) {
return null;
}
return ((PrismContainer<AssignmentType>) assignmentIdi.getItemNew()).getValue().asContainerable();
}
@Override
public QName getRelation() {
return relation;
}
public void setRelation(QName relation) {
this.relation = relation;
}
@Override
public ObjectType getTarget() {
return target;
}
public void setTarget(ObjectType target) {
this.target = target;
}
@Override
public ObjectType getSource() {
return source;
}
@SuppressWarnings("unused")
public String getSourceDescription() {
return sourceDescription;
}
public boolean isPathToSourceValid() {
return pathToSourceValid;
}
public void setPathToSourceValid(boolean pathToSourceValid) {
this.pathToSourceValid = pathToSourceValid;
}
public boolean isValidityOverride() {
return validityOverride;
}
@SuppressWarnings("SameParameterValue")
public void setValidityOverride(boolean validityOverride) {
this.validityOverride = validityOverride;
}
public EvaluationOrder getEvaluationOrder() {
return evaluationOrder;
}
public void setEvaluationOrder(EvaluationOrder evaluationOrder) {
setEvaluationOrder(evaluationOrder, null);
}
public void setEvaluationOrder(EvaluationOrder evaluationOrder, Boolean matchingOrder) {
this.evaluationOrder = evaluationOrder;
this.isMatchingOrder = matchingOrder;
}
public EvaluationOrder getEvaluationOrderForTarget() {
return evaluationOrderForTarget;
}
public void setEvaluationOrderForTarget(EvaluationOrder evaluationOrder) {
setEvaluationOrderForTarget(evaluationOrder, null);
}
public void setEvaluationOrderForTarget(EvaluationOrder evaluationOrderForTarget, Boolean matching) {
this.evaluationOrderForTarget = evaluationOrderForTarget;
this.isMatchingOrderForTarget = matching;
}
public ObjectType getOrderOneObject() {
return varThisObject;
}
public void setOrderOneObject(ObjectType varThisObject) {
this.varThisObject = varThisObject;
}
public boolean isProcessMembership() {
return processMembership;
}
public void setProcessMembership(boolean processMembership) {
this.processMembership = processMembership;
}
@Override
public boolean isMatchingOrder() {
if (isMatchingOrder == null) {
isMatchingOrder = computeMatchingOrder(evaluationOrder, getAssignment());
}
return isMatchingOrder;
}
@Override
public boolean isMatchingOrderForTarget() {
if (isMatchingOrderForTarget == null) {
isMatchingOrderForTarget = computeMatchingOrder(evaluationOrderForTarget, getAssignment());
}
return isMatchingOrderForTarget;
}
static boolean computeMatchingOrder(EvaluationOrder evaluationOrder, AssignmentType assignmentType) {
return computeMatchingOrder(evaluationOrder, assignmentType.getOrder(), assignmentType.getOrderConstraint());
}
static boolean computeMatchingOrder(EvaluationOrder evaluationOrder, Integer assignmentOrder,
List<OrderConstraintsType> assignmentOrderConstraint) {
boolean rv;
List<QName> extraRelations = new ArrayList<>(evaluationOrder.getExtraRelations());
if (assignmentOrder == null && assignmentOrderConstraint.isEmpty()) {
// compatibility
rv = evaluationOrder.getSummaryOrder() == 1;
} else {
rv = true;
if (assignmentOrder != null) {
if (evaluationOrder.getSummaryOrder() != assignmentOrder) {
rv = false;
}
}
for (OrderConstraintsType orderConstraint : assignmentOrderConstraint) {
if (!isMatchingConstraint(orderConstraint, evaluationOrder)) {
rv = false;
break;
}
extraRelations.removeIf(r -> QNameUtil.match(r, orderConstraint.getRelation()));
}
}
if (!extraRelations.isEmpty()) {
rv = false;
}
LOGGER.trace("computeMatchingOrder => {}, for assignment.order={}, assignment.orderConstraint={}, evaluationOrder={}, remainingExtraRelations={}",
rv, assignmentOrder, assignmentOrderConstraint, evaluationOrder, extraRelations);
return rv;
}
private static boolean isMatchingConstraint(OrderConstraintsType orderConstraint, EvaluationOrder evaluationOrder) {
int evaluationOrderInt = evaluationOrder.getMatchingRelationOrder(orderConstraint.getRelation());
if (orderConstraint.getOrder() != null) {
return orderConstraint.getOrder() == evaluationOrderInt;
} else {
int orderMin = 1;
int orderMax = 1;
if (orderConstraint.getOrderMin() != null) {
orderMin = XsdTypeMapper.multiplicityToInteger(orderConstraint.getOrderMin());
}
if (orderConstraint.getOrderMax() != null) {
orderMax = XsdTypeMapper.multiplicityToInteger(orderConstraint.getOrderMax());
}
return XsdTypeMapper.isMatchingMultiplicity(evaluationOrderInt, orderMin, orderMax);
}
}
@Override
public boolean isDelegation() {
return ObjectTypeUtil.isDelegationRelation(relation);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((assignmentIdi == null) ? 0 : assignmentIdi.hashCode());
result = prime * result + ((source == null) ? 0 : source.hashCode());
result = prime * result + ((target == null) ? 0 : target.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AssignmentPathSegmentImpl other = (AssignmentPathSegmentImpl) obj;
if (assignmentIdi == null) {
if (other.assignmentIdi != null)
return false;
} else if (!assignmentIdi.equals(other.assignmentIdi))
return false;
if (source == null) {
if (other.source != null)
return false;
} else if (!source.equals(other.source))
return false;
if (target == null) {
if (other.target != null)
return false;
} else if (!target.equals(other.target))
return false;
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("AssignmentPathSegment(");
sb.append(evaluationOrder);
if (isMatchingOrder()) { // here is a side effect but most probably it's harmless
sb.append("(match)");
}
if (isMatchingOrderForTarget()) { // the same here
sb.append("(match-target)");
}
sb.append(": ");
sb.append(source).append(" ");
if (!isAssignment) {
sb.append("inducement ");
}
PrismContainer<AssignmentType> assignment = (PrismContainer<AssignmentType>) assignmentIdi.getAnyItem();
AssignmentType assignmentType = assignment != null ? assignment.getValue().asContainerable() : null;
if (assignmentType != null) {
sb.append("id:").append(assignmentType.getId()).append(" ");
if (assignmentType.getConstruction() != null) {
sb.append("Constr '").append(assignmentType.getConstruction().getDescription()).append("' ");
}
if (assignmentType.getFocusMappings() != null) {
sb.append("FMappings (").append(assignmentType.getFocusMappings().getMapping().size()).append(") ");
}
if (assignmentType.getPolicyRule() != null) {
sb.append("Rule '").append(assignmentType.getPolicyRule().getName()).append("' ");
}
}
ObjectReferenceType targetRef = assignmentType != null ? assignmentType.getTargetRef() : null;
if (target != null || targetRef != null) {
sb.append("-[");
if (relation != null) {
sb.append(relation.getLocalPart());
}
sb.append("]-> ");
if (target != null) {
sb.append(target);
} else {
sb.append(ObjectTypeUtil.toShortString(targetRef, true));
}
}
// TODO eventually remove this
if (lastEqualOrderSegmentIndex != null) {
sb.append(", lastEqualOrder: ").append(lastEqualOrderSegmentIndex);
}
sb.append(")");
return sb.toString();
}
@Override
public String debugDump() {
return debugDump(0);
}
@Override
public String debugDump(int indent) {
StringBuilder sb = new StringBuilder();
DebugUtil.debugDumpLabel(sb, "AssignmentPathSegment", indent);
sb.append("\n");
DebugUtil.debugDumpWithLabelLn(sb, "source", source==null?"null":source.toString(), indent + 1);
String assignmentOrInducement = isAssignment ? "assignment" : "inducement";
if (assignmentIdi != null) {
DebugUtil.debugDumpWithLabelLn(sb, assignmentOrInducement + " old", String.valueOf(assignmentIdi.getItemOld()), indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, assignmentOrInducement + " delta", String.valueOf(assignmentIdi.getDelta()), indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, assignmentOrInducement + " new", String.valueOf(assignmentIdi.getItemNew()), indent + 1);
} else {
DebugUtil.debugDumpWithLabelLn(sb, assignmentOrInducement, "null", indent + 1);
}
DebugUtil.debugDumpWithLabelLn(sb, "target", target==null?"null":target.toString(), indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "evaluationOrder", evaluationOrder, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "isMatchingOrder", isMatchingOrder, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "isMatchingOrderForTarget", isMatchingOrderForTarget, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "relation", relation, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "pathToSourceValid", pathToSourceValid, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "validityOverride", validityOverride, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "processMembership", processMembership, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "lastEqualOrderSegmentIndex", lastEqualOrderSegmentIndex, indent + 1);
DebugUtil.debugDumpWithLabel(sb, "varThisObject", varThisObject==null?"null":varThisObject.toString(), indent + 1);
return sb.toString();
}
@NotNull
@Override
public AssignmentPathSegmentType toAssignmentPathSegmentType() {
AssignmentPathSegmentType rv = new AssignmentPathSegmentType();
AssignmentType assignment = getAssignment();
if (assignment != null) {
rv.setAssignment(assignment);
rv.setAssignmentId(assignment.getId());
}
if (source != null) {
rv.setSourceRef(ObjectTypeUtil.createObjectRef(source));
rv.setSourceDisplayName(ObjectTypeUtil.getDisplayName(source));
}
if (target != null) {
rv.setTargetRef(ObjectTypeUtil.createObjectRef(target));
rv.setTargetDisplayName(ObjectTypeUtil.getDisplayName(target));
}
rv.setMatchingOrder(isMatchingOrder());
return rv;
}
public Integer getLastEqualOrderSegmentIndex() {
return lastEqualOrderSegmentIndex;
}
public void setLastEqualOrderSegmentIndex(Integer lastEqualOrderSegmentIndex) {
this.lastEqualOrderSegmentIndex = lastEqualOrderSegmentIndex;
}
}