com.evolveum.midpoint.model.impl.lens.projector.focus.InboundProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.midpoint.model.impl.lens.projector.focus.InboundProcessor.java

Source

/*
 * 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.model.impl.lens.projector.focus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.common.filter.Filter;
import com.evolveum.midpoint.common.filter.FilterManager;
import com.evolveum.midpoint.common.refinery.PropertyLimitations;
import com.evolveum.midpoint.common.refinery.RefinedAssociationDefinition;
import com.evolveum.midpoint.common.refinery.RefinedAttributeDefinition;
import com.evolveum.midpoint.common.refinery.RefinedObjectClassDefinition;
import com.evolveum.midpoint.model.api.context.SynchronizationPolicyDecision;
import com.evolveum.midpoint.model.common.mapping.MappingImpl;
import com.evolveum.midpoint.model.common.mapping.MappingFactory;

import com.evolveum.midpoint.model.impl.lens.*;
import com.evolveum.midpoint.model.impl.lens.projector.ContextLoader;
import com.evolveum.midpoint.model.impl.lens.projector.MappingEvaluator;
import com.evolveum.midpoint.model.impl.lens.projector.MappingEvaluatorParams;
import com.evolveum.midpoint.model.impl.lens.projector.MappingInitializer;
import com.evolveum.midpoint.model.impl.lens.projector.MappingOutputProcessor;
import com.evolveum.midpoint.model.impl.lens.projector.MappingTimeEval;
import com.evolveum.midpoint.model.impl.lens.projector.credentials.CredentialsProcessor;
import com.evolveum.midpoint.prism.Item;
import com.evolveum.midpoint.prism.ItemDefinition;
import com.evolveum.midpoint.prism.OriginType;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContainerValue;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.PrismObjectDefinition;
import com.evolveum.midpoint.prism.PrismProperty;
import com.evolveum.midpoint.prism.PrismPropertyDefinition;
import com.evolveum.midpoint.prism.PrismPropertyValue;
import com.evolveum.midpoint.prism.PrismReference;
import com.evolveum.midpoint.prism.PrismValue;
import com.evolveum.midpoint.prism.crypto.EncryptionException;
import com.evolveum.midpoint.prism.crypto.Protector;
import com.evolveum.midpoint.prism.delta.ChangeType;
import com.evolveum.midpoint.prism.delta.ContainerDelta;
import com.evolveum.midpoint.prism.delta.DeltaSetTriple;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.delta.PrismValueDeltaSetTriple;
import com.evolveum.midpoint.prism.delta.PropertyDelta;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.provisioning.api.ProvisioningService;
import com.evolveum.midpoint.repo.common.expression.ExpressionUtil;
import com.evolveum.midpoint.repo.common.expression.ExpressionVariables;
import com.evolveum.midpoint.repo.common.expression.ItemDeltaItem;
import com.evolveum.midpoint.repo.common.expression.Source;
import com.evolveum.midpoint.repo.common.expression.ValuePolicyResolver;
import com.evolveum.midpoint.repo.common.expression.VariableProducer;
import com.evolveum.midpoint.schema.constants.ExpressionConstants;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SecurityViolationException;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ActivationType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AssignmentPolicyEnforcementType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.LayerType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.MappingStrengthType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.MappingType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PasswordType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PropertyAccessType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceBidirectionalMappingAndDefinitionType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceBidirectionalMappingType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowAssociationType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ValueFilterType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ValuePolicyType;
import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType;

import static org.apache.commons.lang3.BooleanUtils.isTrue;

/**
 * Processor that takes changes from accounts and synchronization deltas and updates user attributes if necessary
 * (by creating secondary user object delta {@link ObjectDelta}).
 *
 * @author lazyman
 * @author Radovan Semancik
 */
@Component
public class InboundProcessor {

    private static final String PROCESS_INBOUND_HANDLING = InboundProcessor.class.getName() + ".processInbound";
    private static final Trace LOGGER = TraceManager.getTrace(InboundProcessor.class);

    @Autowired
    private PrismContext prismContext;
    @Autowired
    private FilterManager<Filter> filterManager;
    @Autowired
    private MappingFactory mappingFactory;
    @Autowired
    private ContextLoader contextLoader;
    @Autowired
    private CredentialsProcessor credentialsProcessor;
    @Autowired
    private MappingEvaluator mappingEvaluator;
    @Autowired
    private Protector protector;
    @Autowired
    private ProvisioningService provisioningService;

    //    private Map<ItemDefinition, List<Mapping<?,?>>> mappingsToTarget;

    <O extends ObjectType> void processInbound(LensContext<O> context, XMLGregorianCalendar now, Task task,
            OperationResult result) throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException,
            ConfigurationException, CommunicationException, SecurityViolationException {
        LensFocusContext<O> focusContext = context.getFocusContext();
        if (focusContext == null) {
            LOGGER.trace("Skipping inbound because there is no focus");
            return;
        }
        if (!FocusType.class.isAssignableFrom(focusContext.getObjectTypeClass())) {
            // We can do this only for focus types.
            LOGGER.trace("Skipping inbound because {} is not focal type", focusContext.getObjectTypeClass());
            return;
        }
        processInboundFocal((LensContext<? extends FocusType>) context, task, now, result);
    }

    private <F extends FocusType> void processInboundFocal(LensContext<F> context, Task task,
            XMLGregorianCalendar now, OperationResult result) throws SchemaException, ExpressionEvaluationException,
            ObjectNotFoundException, ConfigurationException, CommunicationException, SecurityViolationException {
        LensFocusContext<F> focusContext = context.getFocusContext();
        if (focusContext == null) {
            LOGGER.trace("Skipping inbound processing because focus is null");
            return;
        }
        if (focusContext.isDelete()) {
            LOGGER.trace("Skipping inbound processing because focus is being deleted");
            return;
        }

        ObjectDelta<F> userSecondaryDelta = focusContext.getProjectionWaveSecondaryDelta();

        if (userSecondaryDelta != null && ChangeType.DELETE.equals(userSecondaryDelta.getChangeType())) {
            //we don't need to do inbound if we are deleting this user
            return;
        }

        OperationResult subResult = result.createMinorSubresult(PROCESS_INBOUND_HANDLING);

        try {
            for (LensProjectionContext projectionContext : context.getProjectionContexts()) {
                if (projectionContext.isThombstone()) {
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace(
                                "Skipping processing of inbound expressions for projection {} because is is thombstone",
                                projectionContext.getHumanReadableName());
                    }
                    continue;
                }
                if (!projectionContext.isCanProject()) {
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace(
                                "Skipping processing of inbound expressions for projection {}: there is a limit to propagate changes only from resource {}",
                                projectionContext.getHumanReadableName(), context.getTriggeredResourceOid());
                    }
                    continue;
                }
                ObjectDelta<ShadowType> aPrioriDelta = getAPrioriDelta(context, projectionContext);

                if (!projectionContext.isDoReconciliation() && aPrioriDelta == null
                        && !LensUtil.hasDependentContext(context, projectionContext)
                        && !projectionContext.isFullShadow() && !projectionContext.isDelete()) {
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("Projection dump\n {}", projectionContext.debugDump());
                        LOGGER.trace(
                                "Skipping processing of inbound expressions for projection {}: no full shadow, no reconciliation, no a priori delta and no dependent context and it's not delete operation",
                                projectionContext.getHumanReadableName());
                    }
                    continue;
                }

                RefinedObjectClassDefinition rOcDef = projectionContext.getCompositeObjectClassDefinition();
                if (rOcDef == null) {
                    LOGGER.error(
                            "Definition for projection {} not found in the context, but it "
                                    + "should be there, dumping context:\n{}",
                            projectionContext.getHumanReadableName(), context.debugDump());
                    throw new IllegalStateException(
                            "Definition for projection " + projectionContext.getHumanReadableName()
                                    + " not found in the context, but it should be there");
                }

                processInboundMappingsForProjection(context, projectionContext, rOcDef, aPrioriDelta, task, now,
                        subResult);
            }

        } finally {
            subResult.computeStatus();
        }
    }

    private boolean isDeleteAccountDelta(LensProjectionContext accountContext) throws SchemaException {
        if (accountContext.getSyncDelta() != null
                && ChangeType.DELETE == accountContext.getSyncDelta().getChangeType()) {
            return true;
        }

        if (accountContext.getDelta() != null && ChangeType.DELETE == accountContext.getDelta().getChangeType()) {
            return true;
        }
        return false;
    }

    private <F extends FocusType, V extends PrismValue, D extends ItemDefinition> void processInboundMappingsForProjection(
            LensContext<F> context, LensProjectionContext projContext,
            RefinedObjectClassDefinition projectionDefinition, ObjectDelta<ShadowType> aPrioriProjectionDelta,
            Task task, XMLGregorianCalendar now, OperationResult result)
            throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException, ConfigurationException,
            CommunicationException, SecurityViolationException {

        if (aPrioriProjectionDelta == null && projContext.getObjectCurrent() == null) {
            LOGGER.trace("Nothing to process in inbound, both a priori delta and current account were null.");
            return;
        }

        PrismObject<ShadowType> accountCurrent = projContext.getObjectCurrent();
        if (hasAnyStrongMapping(projectionDefinition) && !projContext.isFullShadow()
                && !projContext.isThombstone()) {
            LOGGER.trace(
                    "There are strong inbound mapping, but the shadow hasn't be fully loaded yet. Trying to load full shadow now.");
            accountCurrent = loadProjection(context, projContext, task, result, accountCurrent);
            if (projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN) {
                return;
            }
        }

        Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget = new HashMap<>();

        for (QName accountAttributeName : projectionDefinition.getNamesOfAttributesWithInboundExpressions()) {
            boolean cont = processAttributeInbound(accountAttributeName, aPrioriProjectionDelta, projContext,
                    projectionDefinition, context, now, mappingsToTarget, task, result);
            if (!cont) {
                return;
            }
        }

        for (QName accountAttributeName : projectionDefinition.getNamesOfAssociationsWithInboundExpressions()) {
            boolean cont = processAssociationInbound(accountAttributeName, aPrioriProjectionDelta, projContext,
                    projectionDefinition, context, now, mappingsToTarget, task, result);
            if (!cont) {
                return;
            }
        }

        if (isDeleteAccountDelta(projContext)) {
            // we don't need to do inbound if account was deleted
            return;
        }
        processSpecialPropertyInbound(projectionDefinition.getPasswordInbound(),
                SchemaConstants.PATH_PASSWORD_VALUE, SchemaConstants.PATH_PASSWORD_VALUE,
                context.getFocusContext().getObjectNew(), projContext, projectionDefinition, context, now, task,
                result);

        processSpecialPropertyInbound(
                projectionDefinition.getActivationBidirectionalMappingType(ActivationType.F_ADMINISTRATIVE_STATUS),
                SchemaConstants.PATH_ACTIVATION_ADMINISTRATIVE_STATUS, context.getFocusContext().getObjectNew(),
                projContext, projectionDefinition, context, now, task, result);
        processSpecialPropertyInbound(
                projectionDefinition.getActivationBidirectionalMappingType(ActivationType.F_VALID_FROM),
                SchemaConstants.PATH_ACTIVATION_VALID_FROM, context.getFocusContext().getObjectNew(), projContext,
                projectionDefinition, context, now, task, result);
        processSpecialPropertyInbound(
                projectionDefinition.getActivationBidirectionalMappingType(ActivationType.F_VALID_TO),
                SchemaConstants.PATH_ACTIVATION_VALID_TO, context.getFocusContext().getObjectNew(), projContext,
                projectionDefinition, context, now, task, result);

        processAuxiliaryObjectClassInbound(aPrioriProjectionDelta, projContext, projectionDefinition, context, now,
                mappingsToTarget, task, result);

        Collection<ItemDelta<V, D>> deltas = evaluateInboundMapping(mappingsToTarget, context, projContext, task,
                result);

        if (deltas == null) {
            LOGGER.trace("No focus delta produces from inbound mappings");
            return;
        }

        for (ItemDelta<V, D> focusItemDelta : deltas) {
            if (focusItemDelta != null && !focusItemDelta.isEmpty()) {
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Created delta (from inbound expression for {} on {})\n{}",
                            focusItemDelta.getElementName(), projContext.getResource(),
                            focusItemDelta.debugDump(1));
                }
                context.getFocusContext().swallowToProjectionWaveSecondaryDelta(focusItemDelta);
                context.recomputeFocus();
            } else {
                LOGGER.trace("Created delta (from inbound expression for {} on {}) was null or empty.",
                        focusItemDelta.getElementName(), projContext.getResource());
            }
        }
    }

    private <V extends PrismValue, D extends ItemDefinition, F extends FocusType> boolean processAttributeInbound(
            QName accountAttributeName, ObjectDelta<ShadowType> aPrioriProjectionDelta,
            final LensProjectionContext projContext, RefinedObjectClassDefinition projectionDefinition,
            final LensContext<F> context, XMLGregorianCalendar now,
            Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget, Task task, OperationResult result)
            throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException, ConfigurationException,
            SecurityViolationException, CommunicationException {

        PrismObject<ShadowType> projCurrent = projContext.getObjectCurrent();
        PrismObject<ShadowType> projNew = projContext.getObjectNew();

        final ItemDelta<V, D> attributeAPrioriDelta;
        if (aPrioriProjectionDelta != null) {
            attributeAPrioriDelta = aPrioriProjectionDelta
                    .findItemDelta(new ItemPath(SchemaConstants.C_ATTRIBUTES, accountAttributeName));
            if (attributeAPrioriDelta == null && !projContext.isFullShadow()
                    && !LensUtil.hasDependentContext(context, projContext)) {
                LOGGER.trace(
                        "Skipping inbound for {} in {}: Not a full shadow and account a priori delta exists, but doesn't have change for processed property.",
                        accountAttributeName, projContext.getResourceShadowDiscriminator());
                return true;
            }
        } else {
            attributeAPrioriDelta = null;
        }

        RefinedAttributeDefinition<?> attrDef = projectionDefinition.findAttributeDefinition(accountAttributeName);

        if (attrDef.isIgnored(LayerType.MODEL)) {
            LOGGER.trace("Skipping inbound for attribute {} in {} because the attribute is ignored",
                    PrettyPrinter.prettyPrint(accountAttributeName), projContext.getResourceShadowDiscriminator());
            return true;
        }

        List<MappingType> inboundMappingTypes = attrDef.getInboundMappingTypes();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Processing inbound for {} in {}; ({} mappings)",
                    PrettyPrinter.prettyPrint(accountAttributeName), projContext.getResourceShadowDiscriminator(),
                    inboundMappingTypes.size());
        }

        PropertyLimitations limitations = attrDef.getLimitations(LayerType.MODEL);
        if (limitations != null) {
            PropertyAccessType access = limitations.getAccess();
            if (access != null) {
                if (access.isRead() == null || !access.isRead()) {
                    LOGGER.warn("Inbound mapping for non-readable attribute {} in {}, skipping",
                            accountAttributeName, projContext.getHumanReadableName());
                    return true;
                }
            }
        }

        if (inboundMappingTypes.isEmpty()) {
            return true;
        }

        for (MappingType inboundMappingType : inboundMappingTypes) {

            // There are two processing options:
            //
            //  * If we have a delta as an input we will proceed in relative mode, applying mappings on the delta.
            //    This usually happens when a delta comes from a sync notification or if there is a primary projection delta.
            //
            //  * if we do NOT have a delta then we will proceed in absolute mode. In that mode we will apply the
            //    mappings to the absolute projection state that we got from provisioning. This is a kind of "inbound reconciliation".
            //
            // TODO what if there is a priori delta for a given attribute (e.g. ADD one) and
            // we want to reconcile also the existing attribute value? This probably would not work.
            if (inboundMappingType.getStrength() == MappingStrengthType.STRONG) {
                LOGGER.trace(
                        "There is an inbound mapping with strength == STRONG, trying to load full account now.");
                if (!projContext.isFullShadow() && !projContext.isDelete()) {
                    projCurrent = loadProjection(context, projContext, task, result, projCurrent);
                    if (projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN) {
                        return false;
                    }
                }
            }

            if (attributeAPrioriDelta == null && !projContext.isFullShadow()
                    && !LensUtil.hasDependentContext(context, projContext)) {
                LOGGER.trace(
                        "Skipping inbound for {} in {}: Not a full shadow and account a priori delta exists, but doesn't have change for processed property.",
                        accountAttributeName, projContext.getResourceShadowDiscriminator());
                continue;
            }

            PrismObject<F> focus;
            if (context.getFocusContext().getObjectCurrent() != null) {
                focus = context.getFocusContext().getObjectCurrent();
            } else {
                focus = context.getFocusContext().getObjectNew();
            }

            if (attributeAPrioriDelta != null) {
                LOGGER.trace("Processing inbound from a priori delta: {}", aPrioriProjectionDelta);

                PrismProperty oldAccountProperty = null;
                if (projCurrent != null) {
                    oldAccountProperty = projCurrent
                            .findProperty(new ItemPath(ShadowType.F_ATTRIBUTES, accountAttributeName));
                }
                collectMappingsForTargets(context, projContext, inboundMappingType, accountAttributeName,
                        oldAccountProperty, attributeAPrioriDelta, focus, null, mappingsToTarget, task, result);

            } else if (projCurrent != null) {

                projCurrent = loadFullShadowIfNeeded(projContext, projCurrent, context, now, task, result);
                if (projCurrent == null) {
                    return false;
                }

                PrismProperty<?> oldAccountProperty = projCurrent
                        .findProperty(new ItemPath(ShadowType.F_ATTRIBUTES, accountAttributeName));
                LOGGER.trace("Processing inbound from account sync absolute state (currentAccount): {}",
                        oldAccountProperty);
                collectMappingsForTargets(context, projContext, inboundMappingType, accountAttributeName,
                        oldAccountProperty, null, focus, null, mappingsToTarget, task, result);
            }
        }

        return true;
    }

    private <V extends PrismValue, D extends ItemDefinition, F extends FocusType> boolean processAssociationInbound(
            QName accountAttributeName, ObjectDelta<ShadowType> aPrioriProjectionDelta,
            final LensProjectionContext projContext, RefinedObjectClassDefinition projectionDefinition,
            final LensContext<F> context, XMLGregorianCalendar now,
            Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget, Task task, OperationResult result)
            throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException, ConfigurationException,
            SecurityViolationException, CommunicationException {

        PrismObject<ShadowType> projCurrent = projContext.getObjectCurrent();
        PrismObject<ShadowType> projNew = projContext.getObjectNew();

        final ItemDelta<V, D> attributeAPrioriDelta;
        if (aPrioriProjectionDelta != null) {
            attributeAPrioriDelta = aPrioriProjectionDelta.findItemDelta(new ItemPath(ShadowType.F_ASSOCIATION));
            if (attributeAPrioriDelta == null && !projContext.isFullShadow()
                    && !LensUtil.hasDependentContext(context, projContext)) {
                LOGGER.trace(
                        "Skipping inbound for {} in {}: Not a full shadow and account a priori delta exists, but doesn't have change for processed property.",
                        accountAttributeName, projContext.getResourceShadowDiscriminator());
                return true;
            }
        } else {
            attributeAPrioriDelta = null;
        }

        RefinedAssociationDefinition associationDef = projectionDefinition
                .findAssociationDefinition(accountAttributeName);

        //TODO:
        if (associationDef.isIgnored(LayerType.MODEL)) {
            LOGGER.trace("Skipping inbound for association {} in {} because the association is ignored",
                    PrettyPrinter.prettyPrint(accountAttributeName), projContext.getResourceShadowDiscriminator());
            return true;
        }

        List<MappingType> inboundMappingTypes = associationDef.getInboundMappingTypes();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Processing inbound for {} in {}; ({} mappings)",
                    PrettyPrinter.prettyPrint(accountAttributeName), projContext.getResourceShadowDiscriminator(),
                    inboundMappingTypes.size());
        }

        PropertyLimitations limitations = associationDef.getLimitations(LayerType.MODEL);
        if (limitations != null) {
            PropertyAccessType access = limitations.getAccess();
            if (access != null) {
                if (access.isRead() == null || !access.isRead()) {
                    LOGGER.warn("Inbound mapping for non-readable association {} in {}, skipping",
                            accountAttributeName, projContext.getHumanReadableName());
                    return true;
                }
            }
        }

        if (inboundMappingTypes.isEmpty()) {
            return true;
        }

        for (MappingType inboundMappingType : inboundMappingTypes) {

            // There are two processing options:
            //
            //  * If we have a delta as an input we will proceed in relative mode, applying mappings on the delta.
            //    This usually happens when a delta comes from a sync notification or if there is a primary projection delta.
            //
            //  * if we do NOT have a delta then we will proceed in absolute mode. In that mode we will apply the
            //    mappings to the absolute projection state that we got from provisioning. This is a kind of "inbound reconciliation".
            //
            // TODO what if there is a priori delta for a given attribute (e.g. ADD one) and
            // we want to reconcile also the existing attribute value? This probably would not work.
            if (inboundMappingType.getStrength() == MappingStrengthType.STRONG) {
                LOGGER.trace(
                        "There is an association inbound mapping with strength == STRONG, trying to load full account now.");
                if (!projContext.isFullShadow() && !projContext.isDelete()) {
                    projCurrent = loadProjection(context, projContext, task, result, projCurrent);
                    if (projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN) {
                        return false;
                    }
                }
            }

            if (attributeAPrioriDelta == null && !projContext.isFullShadow()
                    && !LensUtil.hasDependentContext(context, projContext)) {
                LOGGER.trace(
                        "Skipping association inbound for {} in {}: Not a full shadow and account a priori delta exists, but doesn't have change for processed property.",
                        accountAttributeName, projContext.getResourceShadowDiscriminator());
                continue;
            }

            PrismObject<F> focus;
            if (context.getFocusContext().getObjectCurrent() != null) {
                focus = context.getFocusContext().getObjectCurrent();
            } else {
                focus = context.getFocusContext().getObjectNew();
            }

            ItemDelta focusItemDelta = null;
            if (attributeAPrioriDelta != null) {
                LOGGER.trace("Processing association inbound from a priori delta: {}", attributeAPrioriDelta);

                PrismContainer<ShadowAssociationType> oldShadowAssociation = projCurrent
                        .findContainer(ShadowType.F_ASSOCIATION);

                PrismContainer<ShadowAssociationType> filteredAssociations = null;
                if (oldShadowAssociation != null) {
                    filteredAssociations = oldShadowAssociation.getDefinition().instantiate();
                    Collection<PrismContainerValue<ShadowAssociationType>> filteredAssociationValues = oldShadowAssociation
                            .getValues().stream()
                            .filter(rVal -> accountAttributeName.equals(rVal.asContainerable().getName()))
                            .map(val -> val.clone()).collect(Collectors.toCollection(ArrayList::new));
                    prismContext.adopt(filteredAssociations);
                    filteredAssociations.addAll(filteredAssociationValues);
                }

                resolveEntitlementsIfNeeded((ContainerDelta<ShadowAssociationType>) attributeAPrioriDelta,
                        filteredAssociations, projContext, task, result);

                VariableProducer<PrismContainerValue<ShadowAssociationType>> entitlementVariable = (value,
                        variables) -> resolveEntitlement(value, projContext, variables);
                collectMappingsForTargets(context, projContext, inboundMappingType, accountAttributeName,
                        (Item) oldShadowAssociation, attributeAPrioriDelta, focus,
                        (VariableProducer) entitlementVariable, mappingsToTarget, task, result);

            } else if (projCurrent != null) {

                projCurrent = loadFullShadowIfNeeded(projContext, projCurrent, context, now, task, result);
                if (projCurrent == null) {
                    LOGGER.trace("Loading of full shadow failed");
                    return false;
                }

                PrismContainer<ShadowAssociationType> oldShadowAssociation = projCurrent
                        .findContainer(ShadowType.F_ASSOCIATION);

                if (oldShadowAssociation == null) {
                    LOGGER.trace("No shadow association value");
                    return true;
                }

                PrismContainer<ShadowAssociationType> filteredAssociations = oldShadowAssociation.getDefinition()
                        .instantiate();
                Collection<PrismContainerValue<ShadowAssociationType>> filteredAssociationValues = oldShadowAssociation
                        .getValues().stream()
                        .filter(rVal -> accountAttributeName.equals(rVal.asContainerable().getName()))
                        .map(val -> val.clone()).collect(Collectors.toCollection(ArrayList::new));
                prismContext.adopt(filteredAssociations);
                filteredAssociations.addAll(filteredAssociationValues);

                resolveEntitlementsIfNeeded((ContainerDelta<ShadowAssociationType>) attributeAPrioriDelta,
                        filteredAssociations, projContext, task, result);

                VariableProducer<PrismContainerValue<ShadowAssociationType>> entitlementVariable = (value,
                        variables) -> resolveEntitlement(value, projContext, variables);
                ;

                LOGGER.trace("Processing association inbound from account sync absolute state (currentAccount): {}",
                        filteredAssociations);
                collectMappingsForTargets(context, projContext, inboundMappingType, accountAttributeName,
                        filteredAssociations, null, focus, entitlementVariable, mappingsToTarget, task, result);

            }
        }

        return true;
    }

    private void resolveEntitlement(PrismContainerValue<ShadowAssociationType> value,
            LensProjectionContext projContext, ExpressionVariables variables) {
        LOGGER.trace("Producing value {} ", value);
        PrismObject<ShadowType> entitlement = projContext.getEntitlementMap()
                .get(value.findReference(ShadowAssociationType.F_SHADOW_REF).getOid());
        LOGGER.trace("Resolved entitlement {}", entitlement);
        if (variables.containsKey(ExpressionConstants.VAR_ENTITLEMENT)) {
            variables.replaceVariableDefinition(ExpressionConstants.VAR_ENTITLEMENT, entitlement);
        } else {
            variables.addVariableDefinition(ExpressionConstants.VAR_ENTITLEMENT, entitlement);
        }
    }

    private <F extends FocusType> void processAuxiliaryObjectClassInbound(
            ObjectDelta<ShadowType> aPrioriProjectionDelta, final LensProjectionContext projContext,
            RefinedObjectClassDefinition projectionDefinition, final LensContext<F> context,
            XMLGregorianCalendar now, Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget, Task task,
            OperationResult result) throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException,
            ConfigurationException, SecurityViolationException, CommunicationException {

        ResourceBidirectionalMappingAndDefinitionType auxiliaryObjectClassMappings = projectionDefinition
                .getAuxiliaryObjectClassMappings();
        if (auxiliaryObjectClassMappings == null) {
            return;
        }
        List<MappingType> inboundMappingTypes = auxiliaryObjectClassMappings.getInbound();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Processing inbound for auxiliary object class in {}; ({} mappings)",
                    projContext.getResourceShadowDiscriminator(), inboundMappingTypes.size());
        }
        if (inboundMappingTypes.isEmpty()) {
            return;
        }

        PrismObject<ShadowType> projCurrent = projContext.getObjectCurrent();
        PrismObject<ShadowType> projNew = projContext.getObjectNew();

        final PropertyDelta<QName> attributeAPrioriDelta;
        if (aPrioriProjectionDelta != null) {
            attributeAPrioriDelta = aPrioriProjectionDelta.findPropertyDelta(ShadowType.F_AUXILIARY_OBJECT_CLASS);
            if (attributeAPrioriDelta == null && !projContext.isFullShadow()
                    && !LensUtil.hasDependentContext(context, projContext)) {
                LOGGER.trace(
                        "Skipping inbound for auxiliary object class in {}: Not a full shadow and account a priori delta exists, but doesn't have change for processed property.",
                        projContext.getResourceShadowDiscriminator());
                return;
            }
        } else {
            attributeAPrioriDelta = null;
        }

        // Make we always have full shadow when dealing with auxiliary object classes.
        // Unlike structural object class the auxiliary object classes may have changed
        // on the resource
        projCurrent = loadFullShadowIfNeeded(projContext, projCurrent, context, now, task, result);
        if (projCurrent == null) {
            return;
        }

        PrismObject<F> focus;
        if (context.getFocusContext().getObjectCurrent() != null) {
            focus = context.getFocusContext().getObjectCurrent();
        } else {
            focus = context.getFocusContext().getObjectNew();
        }

        for (MappingType inboundMappingType : inboundMappingTypes) {

            ItemDelta focusItemDelta = null;

            PrismProperty<QName> oldAccountProperty = projCurrent.findProperty(ShadowType.F_AUXILIARY_OBJECT_CLASS);
            LOGGER.trace("Processing inbound from account sync absolute state (currentAccount): {}",
                    oldAccountProperty);
            collectMappingsForTargets(context, projContext, inboundMappingType, ShadowType.F_AUXILIARY_OBJECT_CLASS,
                    oldAccountProperty, null, focus, null, mappingsToTarget, task, result);
        }
    }

    private <F extends FocusType> PrismObject<ShadowType> loadFullShadowIfNeeded(LensProjectionContext projContext,
            PrismObject<ShadowType> projCurrent, LensContext<F> context, XMLGregorianCalendar now, Task task,
            OperationResult result) throws SchemaException {
        if (!projContext.isFullShadow()) {
            LOGGER.warn(
                    "Attempted to execute inbound expression on account shadow {} WITHOUT full account. Trying to load the account now.",
                    projContext.getOid()); // todo change to trace level eventually
            projCurrent = loadProjection(context, projContext, task, result, projCurrent);
            if (projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN) {
                return null;
            }
            if (!projContext.isFullShadow()) {
                if (projContext.getResourceShadowDiscriminator().getOrder() > 0) {
                    // higher-order context. It is OK not to load this
                    LOGGER.trace(
                            "Skipped load of higher-order account with shadow OID {} skipping inbound processing on it",
                            projContext.getOid());
                    return null;
                }
                // TODO: is it good to mark as broken? what is
                // the resorce is down?? if there is no
                // assignment and the account was added directly
                // it can cause that the account will be
                // unlinked from the user FIXME
                LOGGER.warn(
                        "Couldn't load account with shadow OID {}, setting context as broken and skipping inbound processing on it",
                        projContext.getOid());
                projContext.setSynchronizationPolicyDecision(SynchronizationPolicyDecision.BROKEN);
                return null;
            }
        }
        return projCurrent;
    }

    private <F extends FocusType> PrismObject<ShadowType> loadProjection(LensContext<F> context,
            LensProjectionContext projContext, Task task, OperationResult result,
            PrismObject<ShadowType> accountCurrent) throws SchemaException {
        try {
            contextLoader.loadFullShadow(context, projContext, "inbound", task, result);
            accountCurrent = projContext.getObjectCurrent();
        } catch (ObjectNotFoundException | SecurityViolationException | CommunicationException
                | ConfigurationException | ExpressionEvaluationException e) {
            LOGGER.warn(
                    "Couldn't load account with shadow OID {} because of {}, setting context as broken and skipping inbound processing on it",
                    projContext.getOid(), e.getMessage());
            projContext.setSynchronizationPolicyDecision(SynchronizationPolicyDecision.BROKEN);
        }
        return accountCurrent;
    }

    private boolean hasAnyStrongMapping(RefinedObjectClassDefinition objectDefinition) {

        for (QName attributeName : objectDefinition.getNamesOfAttributesWithInboundExpressions()) {
            RefinedAttributeDefinition<?> attributeDefinition = objectDefinition
                    .findAttributeDefinition(attributeName);
            for (MappingType inboundMapping : attributeDefinition.getInboundMappingTypes()) {
                if (inboundMapping.getStrength() == MappingStrengthType.STRONG) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
    * A priori delta is a delta that was executed in a previous "step". That means it is either delta from a previous
    * wave or a sync delta (in wave 0).
    */
    private <F extends ObjectType> ObjectDelta<ShadowType> getAPrioriDelta(LensContext<F> context,
            LensProjectionContext accountContext) throws SchemaException {
        int wave = context.getProjectionWave();
        if (wave == 0) {
            return accountContext.getSyncDelta();
        }
        if (wave == accountContext.getWave() + 1) {
            // If this resource was processed in a previous wave ....
            // Normally, we take executed delta. However, there are situations (like preview changes - i.e. projector without execution),
            // when there is no executed delta. In that case we take standard primary + secondary delta.
            // TODO is this really correct? Think if the following can happen:
            // - NOT previewing
            // - no executed deltas but
            // - existing primary/secondary delta.
            List<LensObjectDeltaOperation<ShadowType>> executed = accountContext.getExecutedDeltas();
            if (executed != null && !executed.isEmpty()) {
                return executed.get(executed.size() - 1).getObjectDelta();
            } else {
                return accountContext.getDelta();
            }
        }
        return null;
    }

    private <F extends ObjectType> boolean checkWeakSkip(MappingImpl<?, ?> inbound, PrismObject<F> newUser)
            throws SchemaException {
        if (inbound.getStrength() != MappingStrengthType.WEAK) {
            return false;
        }
        if (newUser == null) {
            return false;
        }
        PrismProperty<?> property = newUser.findProperty(inbound.getOutputPath());
        if (property != null && !property.isEmpty()) {
            return true;
        }
        return false;
    }

    private <F extends FocusType, V extends PrismValue, D extends ItemDefinition> void collectMappingsForTargets(
            final LensContext<F> context, LensProjectionContext projectionCtx, MappingType inboundMappingType,
            QName accountAttributeName, Item<V, D> oldAccountProperty, ItemDelta<V, D> attributeAPrioriDelta,
            PrismObject<F> focusNew, VariableProducer<V> variableProducer,
            Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget, Task task, OperationResult result)
            throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, ConfigurationException,
            SecurityViolationException, CommunicationException {

        if (oldAccountProperty != null && oldAccountProperty.hasRaw()) {
            throw new SystemException("Property " + oldAccountProperty
                    + " has raw parsing state, such property cannot be used in inbound expressions");
        }

        ResourceType resource = projectionCtx.getResource();
        MappingImpl.Builder<V, D> builder = mappingFactory.createMappingBuilder(inboundMappingType,
                "inbound expression for " + accountAttributeName + " in " + resource);

        if (!builder.isApplicableToChannel(context.getChannel())) {
            return;
        }

        PrismObject<ShadowType> accountNew = projectionCtx.getObjectNew();
        ExpressionVariables variables = new ExpressionVariables();
        variables.addVariableDefinition(ExpressionConstants.VAR_USER, focusNew);
        variables.addVariableDefinition(ExpressionConstants.VAR_FOCUS, focusNew);
        variables.addVariableDefinition(ExpressionConstants.VAR_ACCOUNT, accountNew);
        variables.addVariableDefinition(ExpressionConstants.VAR_SHADOW, accountNew);
        variables.addVariableDefinition(ExpressionConstants.VAR_RESOURCE, resource);
        variables.addVariableDefinition(ExpressionConstants.VAR_CONFIGURATION, context.getSystemConfiguration());
        variables.addVariableDefinition(ExpressionConstants.VAR_OPERATION,
                context.getFocusContext().getOperation().getValue());

        Source<V, D> defaultSource = new Source<>(oldAccountProperty, attributeAPrioriDelta, null,
                ExpressionConstants.VAR_INPUT);
        defaultSource.recompute();
        builder = builder.defaultSource(defaultSource).targetContext(LensUtil.getFocusDefinition(context))
                .variables(variables).variableResolver(variableProducer)
                .valuePolicyResolver(createStringPolicyResolver(context, task, result))
                .originType(OriginType.INBOUND).originObject(resource);

        if (!context.getFocusContext().isDelete()) {
            Collection<V> originalValues = ExpressionUtil.computeTargetValues(inboundMappingType.getTarget(),
                    focusNew, variables, mappingFactory.getObjectResolver(), "resolving range", task, result);
            builder.originalTargetValues(originalValues);
        }

        MappingImpl<V, D> mapping = builder.build();

        if (checkWeakSkip(mapping, focusNew)) {
            LOGGER.trace("Skipping because of mapping is weak and focus property has already a value");
            return;
        }

        ItemPath targetFocusItemPath = mapping.getOutputPath();
        if (ItemPath.isNullOrEmpty(targetFocusItemPath)) {
            throw new ConfigurationException("Empty target path in " + mapping.getContextDescription());
        }
        boolean isAssignment = new ItemPath(FocusType.F_ASSIGNMENT).equivalent(targetFocusItemPath);
        Item targetFocusItem = null;
        if (focusNew != null) {
            targetFocusItem = focusNew.findItem(targetFocusItemPath);
        }
        PrismObjectDefinition<F> focusDefinition = context.getFocusContext().getObjectDefinition();
        ItemDefinition targetItemDef = focusDefinition.findItemDefinition(targetFocusItemPath);
        if (targetItemDef == null) {
            throw new SchemaException("No definition for focus property " + targetFocusItemPath
                    + ", cannot process inbound expression in " + resource);
        }

        List<MappingImpl<V, D>> existingMapping = (List) mappingsToTarget.get(targetItemDef);
        if (CollectionUtils.isEmpty(existingMapping)) {
            mappingsToTarget.put(targetItemDef, Arrays.asList(mapping));
        } else {
            List<MappingImpl<V, D>> clone = new ArrayList<>(existingMapping);
            clone.add(mapping);
            mappingsToTarget.replace(targetItemDef, (List) clone);
        }
    }

    private <F extends FocusType, V extends PrismValue, D extends ItemDefinition> Collection<ItemDelta<V, D>> evaluateInboundMapping(
            Map<ItemDefinition, List<MappingImpl<?, ?>>> mappingsToTarget, final LensContext<F> context,
            LensProjectionContext projectionCtx, Task task, OperationResult result)
            throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, ConfigurationException,
            SecurityViolationException, CommunicationException {

        PrismObject<F> focusNew = context.getFocusContext().getObjectCurrent();
        if (focusNew == null) {
            focusNew = context.getFocusContext().getObjectNew();
        }

        Collection<ItemDelta<V, D>> outputDeltas = new ArrayList<>();

        Set<Entry<D, List<MappingImpl<V, D>>>> mappingsToTargeSet = (Set) mappingsToTarget.entrySet();
        for (Entry<D, List<MappingImpl<V, D>>> mappingEntry : mappingsToTargeSet) {
            checkTolerant(mappingEntry.getValue());

            List<MappingImpl<V, D>> mappings = mappingEntry.getValue();
            Iterator<MappingImpl<V, D>> mappingIterator = mappings.iterator();
            DeltaSetTriple<ItemValueWithOrigin<V, D>> allTriples = new DeltaSetTriple<>();
            while (mappingIterator.hasNext()) {
                MappingImpl<V, D> mapping = mappingIterator.next();
                mappingEvaluator.evaluateMapping(mapping, context, projectionCtx, task, result);

                DeltaSetTriple<ItemValueWithOrigin<V, D>> itemValueWithOrigin = ItemValueWithOrigin
                        .createOutputTriple(mapping);
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Inbound mapping for {}\nreturned triple:\n{}",
                            mapping.getDefaultSource().debugDump(),
                            itemValueWithOrigin == null ? "null" : itemValueWithOrigin.debugDump());
                }
                if (itemValueWithOrigin != null) {
                    allTriples.addAllToMinusSet(itemValueWithOrigin.getMinusSet());
                    allTriples.addAllToPlusSet(itemValueWithOrigin.getPlusSet());
                    allTriples.addAllToZeroSet(itemValueWithOrigin.getZeroSet());
                }
            }
            AssignmentPolicyEnforcementType assignmentEnforcement = projectionCtx
                    .getAssignmentPolicyEnforcementType();
            DeltaSetTriple<ItemValueWithOrigin<V, D>> consolidatedTriples = consolidateTriples(allTriples,
                    assignmentEnforcement);

            LOGGER.trace("Consolidated triples {} \nfor mapping for item {}", consolidatedTriples.debugDumpLazily(),
                    mappingEntry.getKey());

            MappingImpl<V, D> firstMapping = mappingEntry.getValue().iterator().next();
            outputDeltas.add(collectOutputDelta(mappingEntry.getKey(), firstMapping.getOutputPath(), focusNew,
                    consolidatedTriples, isTrue(firstMapping.isTolerant()), hasRange(mappingEntry.getValue()),
                    projectionCtx.isDelete()));
        }

        // if no changes were generated return null
        return outputDeltas.isEmpty() ? null : outputDeltas;
    }

    private <V extends PrismValue, D extends ItemDefinition> DeltaSetTriple<ItemValueWithOrigin<V, D>> consolidateTriples(
            DeltaSetTriple<ItemValueWithOrigin<V, D>> originTriples, AssignmentPolicyEnforcementType enforcement) {
        // Meaning of the resulting triple:
        // values in PLUS set will be added (valuesToAdd in delta)
        // values in MINUS set will be removed (valuesToDelete in delta)
        // values in ZERO set will be compared with existing values in
        // user property
        // the differences will be added to delta

        Collection<ItemValueWithOrigin<V, D>> consolidatedZeroSet = new HashSet<>();
        Collection<ItemValueWithOrigin<V, D>> consolidatedPlusSet = new HashSet<>();
        Collection<ItemValueWithOrigin<V, D>> consolidatedMinusSet = new HashSet<>();

        if (originTriples != null) {
            if (originTriples.hasPlusSet()) {
                Collection<ItemValueWithOrigin<V, D>> plusSet = originTriples.getPlusSet();
                LOGGER.trace("Consolidating plusSet from origin:\n {}", DebugUtil.debugDumpLazily(plusSet));

                for (ItemValueWithOrigin<V, D> plusValue : plusSet) {
                    boolean consolidated = false;
                    if (originTriples.hasMinusSet()) {
                        for (ItemValueWithOrigin<V, D> minusValue : originTriples.getMinusSet()) {
                            if (minusValue.getItemValue().equalsRealValue(plusValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from minus set -> moved to the zero, becuase the same value present in plus and minus set at the same time",
                                        minusValue.debugDumpLazily());
                                consolidatedMinusSet.remove(minusValue);
                                consolidatedPlusSet.remove(plusValue);
                                consolidatedZeroSet.add(minusValue);
                                consolidated = true;
                            }
                        }
                    }

                    if (originTriples.hasZeroSet()) {
                        for (ItemValueWithOrigin<V, D> zeroValue : originTriples.getZeroSet()) {
                            if (zeroValue.getItemValue().equalsRealValue(plusValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from plus set -> moved to the zero, becuase the same value present in plus and minus set at the same time",
                                        zeroValue.debugDumpLazily());
                                consolidatedPlusSet.remove(plusValue);
                                consolidated = true;
                            }
                        }
                    }
                    if (!consolidated) { //&& !PrismValue.containsRealValue(consolidatedPlusSet, plusValue)) {
                        consolidatedPlusSet.add(plusValue);
                    }
                }

            }

            if (originTriples.hasZeroSet()) {
                Collection<ItemValueWithOrigin<V, D>> zeroSet = originTriples.getZeroSet();
                LOGGER.trace("Consolidating zero set from origin:\n {}", DebugUtil.debugDumpLazily(zeroSet));

                for (ItemValueWithOrigin<V, D> zeroValue : zeroSet) {
                    boolean consolidated = false;
                    if (originTriples.hasMinusSet()) {
                        for (ItemValueWithOrigin<V, D> minusValue : originTriples.getMinusSet()) {
                            if (minusValue.getItemValue().equalsRealValue(zeroValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from minus set -> moved to the zero, becuase the same value present in zero and minus set at the same time",
                                        minusValue.debugDumpLazily());
                                consolidatedMinusSet.remove(minusValue);
                                consolidatedZeroSet.add(minusValue);
                                consolidated = true;
                            }
                        }

                    }

                    if (originTriples.hasPlusSet()) {
                        for (ItemValueWithOrigin<V, D> plusValue : originTriples.getPlusSet()) {
                            if (plusValue.getItemValue().equalsRealValue(zeroValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from plus set -> moved to the zero, becuase the same value present in zero and plus set at the same time",
                                        plusValue.debugDumpLazily());
                                consolidatedPlusSet.remove(plusValue);
                                consolidatedZeroSet.add(plusValue);
                                consolidated = true;
                            }
                        }

                    }
                    if (!consolidated) {// && !PrismValue.containsRealValue(consolidatedZeroSet, zeroValue)) {
                        consolidatedZeroSet.add(zeroValue);
                    }
                }

            }

            if (originTriples.hasMinusSet()) {
                Collection<ItemValueWithOrigin<V, D>> minusSet = originTriples.getMinusSet();
                LOGGER.trace("Consolidating minus set from origin:\n {}", DebugUtil.debugDumpLazily(minusSet));

                for (ItemValueWithOrigin<V, D> minusValue : minusSet) {
                    boolean consolidated = false;
                    if (originTriples.hasPlusSet()) {
                        for (ItemValueWithOrigin<V, D> plusValue : originTriples.getPlusSet()) {
                            if (plusValue.getItemValue().equalsRealValue(minusValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from minus set -> moved to the zero, becuase the same value present in plus and minus set at the same time",
                                        plusValue.debugDumpLazily());
                                consolidatedPlusSet.remove(plusValue);
                                consolidatedMinusSet.remove(minusValue);
                                consolidatedZeroSet.add(minusValue);
                                consolidated = true;
                            }
                        }
                    }

                    if (originTriples.hasZeroSet()) {
                        for (ItemValueWithOrigin<V, D> zeroValue : originTriples.getZeroSet()) {
                            if (zeroValue.getItemValue().equalsRealValue(minusValue.getItemValue())) {
                                LOGGER.trace(
                                        "Removing value {} from minus set -> moved to the zero, becuase the same value present in plus and minus set at the same time",
                                        zeroValue.debugDumpLazily());
                                consolidatedMinusSet.remove(minusValue);
                                consolidatedZeroSet.add(zeroValue);
                                consolidated = true;
                            }
                        }
                    }

                    if (!consolidated) { // && !PrismValue.containsRealValue(consolidatedMinusSet, minusValue)) {
                        consolidatedMinusSet.add(minusValue);
                    }

                }

            }
        }

        DeltaSetTriple<ItemValueWithOrigin<V, D>> consolidatedTriples = new DeltaSetTriple<>();
        consolidatedTriples.addAllToMinusSet(consolidatedMinusSet);
        consolidatedTriples.addAllToPlusSet(consolidatedPlusSet);
        consolidatedTriples.addAllToZeroSet(consolidatedZeroSet);
        return consolidatedTriples;
    }

    private <V extends PrismValue, D extends ItemDefinition, F extends FocusType> ItemDelta<V, D> collectOutputDelta(
            ItemDefinition outputDefinition, ItemPath outputPath, PrismObject<F> focusNew,
            DeltaSetTriple<ItemValueWithOrigin<V, D>> consolidatedTriples, boolean tolerant, boolean hasRange,
            boolean isDelete) throws SchemaException {

        ItemDelta outputFocusItemDelta = outputDefinition.createEmptyDelta(outputPath);
        Item targetFocusItem = null;
        if (focusNew != null) {
            targetFocusItem = focusNew.findItem(outputPath);
        }
        boolean isAssignment = new ItemPath(FocusType.F_ASSIGNMENT).equivalent(outputPath);

        Item shouldBeItem = outputDefinition.instantiate();
        if (consolidatedTriples != null) {

            Collection<ItemValueWithOrigin<V, D>> shouldBeItemValues = consolidatedTriples.getNonNegativeValues();
            for (ItemValueWithOrigin<V, D> itemWithOrigin : shouldBeItemValues) {
                shouldBeItem.add(LensUtil.cloneAndApplyMetadata(itemWithOrigin.getItemValue(), isAssignment,
                        shouldBeItemValues));
            }

            if (consolidatedTriples.hasPlusSet()) {

                boolean alreadyReplaced = false;
                for (ItemValueWithOrigin<V, D> valueWithOrigin : consolidatedTriples.getPlusSet()) {
                    MappingImpl<V, D> originMapping = (MappingImpl) valueWithOrigin.getMapping();
                    if (targetFocusItem == null) {
                        targetFocusItem = focusNew.findItem(originMapping.getOutputPath());
                    }
                    V value = valueWithOrigin.getItemValue();
                    if (targetFocusItem != null && targetFocusItem.hasRealValue(value)) {
                        continue;
                    }

                    if (outputFocusItemDelta == null) {
                        outputFocusItemDelta = outputDefinition.createEmptyDelta(originMapping.getOutputPath());
                    }

                    // if property is not multi value replace existing
                    // attribute
                    if (targetFocusItem != null && !targetFocusItem.getDefinition().isMultiValue()
                            && !targetFocusItem.isEmpty()) {
                        Collection<V> replace = new ArrayList<>();
                        replace.add(LensUtil.cloneAndApplyMetadata(value, isAssignment,
                                originMapping.getMappingType()));
                        outputFocusItemDelta.setValuesToReplace(replace);

                        if (alreadyReplaced) {
                            LOGGER.warn("Multiple values for a single-valued property {}; duplicate value = {}",
                                    targetFocusItem, value);
                        } else {
                            alreadyReplaced = true;
                        }
                    } else {
                        outputFocusItemDelta.addValueToAdd(LensUtil.cloneAndApplyMetadata(value, isAssignment,
                                originMapping.getMappingType()));
                    }
                }
            }

            if (consolidatedTriples.hasMinusSet()) {
                LOGGER.trace("Checking account sync property delta values to delete");
                for (ItemValueWithOrigin<V, D> valueWithOrigin : consolidatedTriples.getMinusSet()) {
                    V value = valueWithOrigin.getItemValue();

                    if (targetFocusItem == null || targetFocusItem.hasRealValue(value)) {
                        if (!outputFocusItemDelta.isReplace()) {
                            // This is not needed if we are going to
                            // replace. In fact it might cause an error.
                            outputFocusItemDelta.addValueToDelete(value);
                        }
                    }
                }
            }

        } else {
            // triple == null
            // the mapping is not applicable. Nothing to do.
        }

        if (isDelete) {
            LOGGER.trace(
                    "Skipping comparison of user's property to produces delta. Projection is going to be deleted, clean up just attributes from this projection.");
            return outputFocusItemDelta;
        }

        if (targetFocusItem != null) {
            ItemDelta diffDelta = targetFocusItem.diff(shouldBeItem);
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Comparing focus item:\n{}\nto should be item:\n{}\ndiff:\n{} ",
                        DebugUtil.debugDump(targetFocusItem, 1), DebugUtil.debugDump(shouldBeItem, 1),
                        DebugUtil.debugDump(diffDelta, 1));
            }

            if (diffDelta != null) {
                // this is probably not correct, as the default for
                // inbounds should be TRUE
                if (tolerant || hasRange) {
                    if (diffDelta.isReplace()) {
                        if (diffDelta.getValuesToReplace().isEmpty()) {
                            diffDelta.resetValuesToReplace();
                            if (LOGGER.isTraceEnabled()) {
                                LOGGER.trace(
                                        "Removing empty replace part of the diff delta because mapping is tolerant:\n{}",
                                        diffDelta.debugDump());
                            }
                        } else {
                            if (LOGGER.isTraceEnabled()) {
                                LOGGER.trace(
                                        "Making sure that the replace part of the diff contains old values delta because mapping is tolerant:\n{}",
                                        diffDelta.debugDump());
                            }
                            for (Object shouldBeValueObj : shouldBeItem.getValues()) {
                                PrismValue shouldBeValue = (PrismValue) shouldBeValueObj;
                                if (!PrismValue.containsRealValue(diffDelta.getValuesToReplace(), shouldBeValue)) {
                                    diffDelta.addValueToReplace(shouldBeValue.clone());
                                }
                            }
                        }
                    } else {
                        diffDelta.resetValuesToDelete();
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(
                                    "Removing delete part of the diff delta because mapping settings are tolerant={}, hasRange={}:\n{}",
                                    tolerant, hasRange, diffDelta.debugDump());
                        }
                    }
                }

                // if (!hasRange(mappingEntry.getValue())) {
                diffDelta.setElementName(ItemPath.getName(outputPath.last()));
                diffDelta.setParentPath(outputPath.allExceptLast());
                outputFocusItemDelta.merge(diffDelta);
                // }
            }

        } else {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(
                        "Adding user property because inbound say so (account doesn't contain that value):\n{}",
                        shouldBeItem.getValues());
            }
            // if user property doesn't exist we have to add it (as
            // delta), because inbound say so
            outputFocusItemDelta.addValuesToAdd(shouldBeItem.getClonedValues());
        }

        return outputFocusItemDelta;
    }

    private <V extends PrismValue, D extends ItemDefinition> boolean hasRange(List<MappingImpl<V, D>> mappings) {
        for (MappingImpl<V, D> mapping : mappings) {
            if (mapping.hasTargetRange()) {
                return true;
            }
        }
        return false;
    }

    private <V extends PrismValue, D extends ItemDefinition> void checkTolerant(List<MappingImpl<V, D>> mappings)
            throws SchemaException {
        int tolerantCount = 0;
        int intolerantCount = 0;
        for (MappingImpl<V, D> mapping : mappings) {
            if (mapping.isTolerant() == Boolean.TRUE) {
                tolerantCount++;
            }
            if (mapping.isTolerant() == null || mapping.isTolerant() == Boolean.FALSE) {
                intolerantCount++;
            }
        }

        if (tolerantCount > 0 && intolerantCount > 0) {
            throw new SchemaException(
                    "Incorrect configuration. There cannot be different 'tolernt' settings for the target item "
                            + mappings.iterator().next().getOutputDefinition());
        }

    }

    private void resolveEntitlementsIfNeeded(ContainerDelta<ShadowAssociationType> attributeAPrioriDelta,
            PrismContainer<ShadowAssociationType> oldAccountProperty, LensProjectionContext projCtx, Task task,
            OperationResult result) {
        Collection<PrismContainerValue<ShadowAssociationType>> shadowAssociations = new ArrayList<>();
        if (oldAccountProperty != null) {
            shadowAssociations.addAll(oldAccountProperty.getValues());
        }

        if (attributeAPrioriDelta != null) {
            shadowAssociations.addAll(attributeAPrioriDelta.getValues(ShadowAssociationType.class));
        }

        if (shadowAssociations == null) {
            LOGGER.trace("No shadow associations found");
            return;
        }

        for (PrismContainerValue<ShadowAssociationType> value : shadowAssociations) {
            PrismReference shadowRef = value.findReference(ShadowAssociationType.F_SHADOW_REF);
            if (shadowRef == null) {
                continue;
            }

            if (projCtx.getEntitlementMap().containsKey(shadowRef.getOid())) {
                shadowRef.getValue().setObject(projCtx.getEntitlementMap().get(shadowRef.getOid()));
            } else {
                try {
                    PrismObject<ShadowType> entitlement = provisioningService.getObject(ShadowType.class,
                            shadowRef.getOid(), null, task, result);
                    projCtx.getEntitlementMap().put(entitlement.getOid(), entitlement);
                } catch (ObjectNotFoundException | CommunicationException | SchemaException | ConfigurationException
                        | SecurityViolationException | ExpressionEvaluationException e) {
                    LOGGER.error("failed to load entitlement.");
                    // TODO: can we just ignore and continue?
                    continue;
                }
            }
        }

    }

    private <F extends ObjectType> ValuePolicyResolver createStringPolicyResolver(final LensContext<F> context,
            final Task task, final OperationResult result) {
        ValuePolicyResolver stringPolicyResolver = new ValuePolicyResolver() {
            private ItemPath outputPath;
            private ItemDefinition outputDefinition;

            @Override
            public void setOutputPath(ItemPath outputPath) {
                this.outputPath = outputPath;
            }

            @Override
            public void setOutputDefinition(ItemDefinition outputDefinition) {
                this.outputDefinition = outputDefinition;
            }

            @Override
            public ValuePolicyType resolve() {
                if (!outputDefinition.getName().equals(PasswordType.F_VALUE)) {
                    return null;
                }
                ValuePolicyType passwordPolicy = credentialsProcessor
                        .determinePasswordPolicy(context.getFocusContext(), task, result);
                if (passwordPolicy == null) {
                    return null;
                }
                return passwordPolicy;
            }
        };
        return stringPolicyResolver;
    }

    private <T> PrismPropertyValue<T> filterValue(PrismPropertyValue<T> propertyValue,
            List<ValueFilterType> filters) {
        PrismPropertyValue<T> filteredValue = propertyValue.clone();
        filteredValue.setOriginType(OriginType.INBOUND);

        if (filters == null || filters.isEmpty()) {
            return filteredValue;
        }

        for (ValueFilterType filter : filters) {
            Filter filterInstance = filterManager.getFilterInstance(filter.getType(), filter.getAny());
            filterInstance.apply(filteredValue);
        }

        return filteredValue;
    }

    /**
      * Processing for special (fixed-schema) properties such as credentials and activation.
     * @throws ObjectNotFoundException
      */
    private <F extends FocusType> void processSpecialPropertyInbound(ResourceBidirectionalMappingType biMappingType,
            ItemPath sourceTargetPath, PrismObject<F> newUser, LensProjectionContext projCtx,
            RefinedObjectClassDefinition rOcDef, LensContext<F> context, XMLGregorianCalendar now, Task task,
            OperationResult opResult) throws SchemaException, ExpressionEvaluationException,
            ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException {
        if (biMappingType == null) {
            return;
        }
        processSpecialPropertyInbound(biMappingType.getInbound(), sourceTargetPath, sourceTargetPath, newUser,
                projCtx, rOcDef, context, now, task, opResult);
    }

    //    private void processSpecialPropertyInbound(MappingType inboundMappingType, ItemPath sourcePath,
    //            PrismObject<UserType> newUser, LensProjectionContext<ShadowType> accContext,
    //            RefinedObjectClassDefinition accountDefinition, LensContext<UserType,ShadowType> context,
    //            OperationResult opResult) throws SchemaException {
    //       if (inboundMappingType == null) {
    //          return;
    //       }
    //       Collection<MappingType> inboundMappingTypes = new ArrayList<MappingType>(1);
    //       inboundMappingTypes.add(inboundMappingType);
    //       processSpecialPropertyInbound(inboundMappingTypes, sourcePath, newUser, accContext, accountDefinition, context, opResult);
    //    }

    /**
     * Processing for special (fixed-schema) properties such as credentials and activation.
     * @throws ObjectNotFoundException
     * @throws ExpressionEvaluationException
     */
    private <F extends FocusType> void processSpecialPropertyInbound(Collection<MappingType> inboundMappingTypes,
            final ItemPath sourcePath, final ItemPath targetPath, final PrismObject<F> newUser,
            final LensProjectionContext projContext, RefinedObjectClassDefinition projectionDefinition,
            final LensContext<F> context, XMLGregorianCalendar now, Task task, OperationResult opResult)
            throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException, CommunicationException,
            ConfigurationException, SecurityViolationException {

        if (inboundMappingTypes == null || inboundMappingTypes.isEmpty() || newUser == null
                || !projContext.isFullShadow()) {
            return;
        }

        ObjectDelta<F> userPrimaryDelta = context.getFocusContext().getPrimaryDelta();
        PropertyDelta primaryPropDelta = null;
        if (userPrimaryDelta != null) {
            primaryPropDelta = userPrimaryDelta.findPropertyDelta(targetPath);
            if (primaryPropDelta != null && primaryPropDelta.isReplace()) {
                // Replace primary delta overrides any inbound
                return;
            }
        }

        ObjectDelta<F> userSecondaryDelta = context.getFocusContext().getProjectionWaveSecondaryDelta();
        if (userSecondaryDelta != null) {
            PropertyDelta<?> delta = userSecondaryDelta.findPropertyDelta(targetPath);
            if (delta != null) {
                //remove delta if exists, it will be handled by inbound
                userSecondaryDelta.getModifications().remove(delta);
            }
        }

        MappingInitializer initializer = (builder) -> {
            if (projContext.getObjectNew() == null) {
                projContext.recompute();
                if (projContext.getObjectNew() == null) {
                    // Still null? something must be really wrong here.
                    String message = "Recomputing account " + projContext.getResourceShadowDiscriminator()
                            + " results in null new account. Something must be really broken.";
                    LOGGER.error(message);
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("Account context:\n{}", projContext.debugDump());
                    }
                    throw new SystemException(message);
                }
            }

            ObjectDelta<ShadowType> aPrioriShadowDelta = getAPrioriDelta(context, projContext);
            ItemDelta<PrismPropertyValue<?>, PrismPropertyDefinition<?>> specialAttributeDelta = null;
            if (aPrioriShadowDelta != null) {
                specialAttributeDelta = aPrioriShadowDelta.findItemDelta(sourcePath);
            }
            ItemDeltaItem<PrismPropertyValue<?>, PrismPropertyDefinition<?>> sourceIdi = projContext
                    .getObjectDeltaObject().findIdi(sourcePath);
            if (specialAttributeDelta == null) {
                specialAttributeDelta = sourceIdi.getDelta();
            }
            Source<PrismPropertyValue<?>, PrismPropertyDefinition<?>> source = new Source<>(sourceIdi.getItemOld(),
                    specialAttributeDelta, sourceIdi.getItemOld(), ExpressionConstants.VAR_INPUT);
            builder = builder.defaultSource(source).addVariableDefinition(ExpressionConstants.VAR_USER, newUser)
                    .addVariableDefinition(ExpressionConstants.VAR_FOCUS, newUser);

            PrismObject<ShadowType> accountNew = projContext.getObjectNew();
            builder = builder.addVariableDefinition(ExpressionConstants.VAR_ACCOUNT, accountNew)
                    .addVariableDefinition(ExpressionConstants.VAR_SHADOW, accountNew)
                    .addVariableDefinition(ExpressionConstants.VAR_RESOURCE, projContext.getResource())
                    .valuePolicyResolver(createStringPolicyResolver(context, task, opResult))
                    .originType(OriginType.INBOUND).originObject(projContext.getResource());

            return builder;
        };

        MappingOutputProcessor<PrismValue> processor = (mappingOutputPath, outputStruct) -> {
            PrismValueDeltaSetTriple<PrismValue> outputTriple = outputStruct.getOutputTriple();
            if (outputTriple == null) {
                LOGGER.trace(
                        "Mapping for property {} evaluated to null. Skipping inboud processing for that property.",
                        sourcePath);
                return false;
            }

            ObjectDelta<F> userSecondaryDeltaInt = context.getFocusContext().getProjectionWaveSecondaryDelta();
            if (userSecondaryDeltaInt != null) {
                PropertyDelta<?> delta = userSecondaryDeltaInt.findPropertyDelta(targetPath);
                if (delta != null) {
                    //remove delta if exists, it will be handled by inbound
                    userSecondaryDeltaInt.getModifications().remove(delta);
                }
            }

            PrismObjectDefinition<F> focusDefinition = context.getFocusContext().getObjectDefinition();
            PrismProperty result = focusDefinition.findPropertyDefinition(targetPath).instantiate();
            result.addAll(PrismValue.cloneCollection(outputTriple.getNonNegativeValues()));

            PrismProperty targetPropertyNew = newUser.findOrCreateProperty(targetPath);
            PropertyDelta<?> delta;
            if (ProtectedStringType.COMPLEX_TYPE.equals(targetPropertyNew.getDefinition().getTypeName())) {
                // We have to compare this in a special way. The cipherdata may be different due to a different
                // IV, but the value may still be the same
                ProtectedStringType resultValue = (ProtectedStringType) result.getRealValue();
                ProtectedStringType targetPropertyNewValue = (ProtectedStringType) targetPropertyNew.getRealValue();
                try {
                    if (protector.compare(resultValue, targetPropertyNewValue)) {
                        delta = null;
                    } else {
                        delta = targetPropertyNew.diff(result);
                    }
                } catch (EncryptionException e) {
                    throw new SystemException(e.getMessage(), e);
                }
            } else {
                delta = targetPropertyNew.diff(result);
            }
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("targetPropertyNew:\n{}\ndelta:\n{}", targetPropertyNew.debugDump(1),
                        DebugUtil.debugDump(delta, 1));
            }
            if (delta != null && !delta.isEmpty()) {
                delta.setParentPath(targetPath.allExceptLast());
                if (!context.getFocusContext().alreadyHasDelta(delta)) {
                    context.getFocusContext().swallowToProjectionWaveSecondaryDelta(delta);
                }
            }
            return false;
        };

        MappingEvaluatorParams<PrismValue, ItemDefinition, F, F> params = new MappingEvaluatorParams<>();
        params.setMappingTypes(inboundMappingTypes);
        params.setMappingDesc("inbound mapping for " + sourcePath + " in " + projContext.getResource());
        params.setNow(now);
        params.setInitializer(initializer);
        params.setProcessor(processor);
        params.setAPrioriTargetObject(newUser);
        params.setAPrioriTargetDelta(userPrimaryDelta);
        params.setTargetContext(context.getFocusContext());
        params.setDefaultTargetItemPath(targetPath);
        params.setEvaluateCurrent(MappingTimeEval.CURRENT);
        params.setContext(context);
        params.setHasFullTargetObject(true);
        mappingEvaluator.evaluateMappingSetProjection(params, task, opResult);

        //        MutableBoolean strongMappingWasUsed = new MutableBoolean();
        //        PrismValueDeltaSetTriple<? extends PrismPropertyValue<?>> outputTriple = mappingEvaluatorHelper.evaluateMappingSetProjection(
        //                inboundMappingTypes, "inbound mapping for " + sourcePath + " in " + accContext.getResource(), now, initializer, targetPropertyNew, primaryPropDelta, newUser, true, strongMappingWasUsed, context, accContext, task, opResult);

    }

    //    private Collection<Mapping> getMappingApplicableToChannel(
    //         Collection<MappingType> inboundMappingTypes, String description, String channelUri) {
    //       Collection<Mapping> inboundMappings = new ArrayList<Mapping>();
    //      for (MappingType inboundMappingType : inboundMappingTypes){
    //         Mapping<PrismPropertyValue<?>,PrismPropertyDefinition<?>> mapping = mappingFactory.createMapping(inboundMappingType,
    //                 description);
    //
    //         if (mapping.isApplicableToChannel(channelUri)){
    //            inboundMappings.add(mapping);
    //         }
    //      }
    //
    //      return inboundMappings;
    //   }
}