com.evolveum.midpoint.provisioning.ucf.impl.connid.ConnectorInstanceConnIdImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.midpoint.provisioning.ucf.impl.connid.ConnectorInstanceConnIdImpl.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.provisioning.ucf.impl.connid;

import static com.evolveum.midpoint.provisioning.ucf.impl.connid.ConnIdUtil.processConnIdException;

import java.util.*;

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

import com.evolveum.midpoint.prism.*;
import com.evolveum.midpoint.prism.query.ObjectPaging;
import com.evolveum.midpoint.prism.query.OrderDirection;
import com.evolveum.midpoint.schema.SearchResultMetadata;
import com.evolveum.midpoint.schema.processor.*;
import com.evolveum.midpoint.schema.statistics.ConnectorOperationalStatus;
import com.evolveum.midpoint.schema.statistics.ProvisioningOperation;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.Holder;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.AddRemoveAttributeValuesCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.PagedSearchCapabilityType;
import com.evolveum.prism.xml.ns._public.query_3.OrderDirectionType;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.identityconnectors.common.pooling.ObjectPoolConfiguration;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.api.APIConfiguration;
import org.identityconnectors.framework.api.ConnectorFacade;
import org.identityconnectors.framework.api.ConnectorFacadeFactory;
import org.identityconnectors.framework.api.ConnectorInfo;
import org.identityconnectors.framework.api.operations.APIOperation;
import org.identityconnectors.framework.api.operations.CreateApiOp;
import org.identityconnectors.framework.api.operations.DeleteApiOp;
import org.identityconnectors.framework.api.operations.GetApiOp;
import org.identityconnectors.framework.api.operations.SchemaApiOp;
import org.identityconnectors.framework.api.operations.ScriptOnConnectorApiOp;
import org.identityconnectors.framework.api.operations.ScriptOnResourceApiOp;
import org.identityconnectors.framework.api.operations.SearchApiOp;
import org.identityconnectors.framework.api.operations.SyncApiOp;
import org.identityconnectors.framework.api.operations.TestApiOp;
import org.identityconnectors.framework.api.operations.UpdateApiOp;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeInfo;
import org.identityconnectors.framework.common.objects.AttributeInfo.Flags;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.ObjectClassInfo;
import org.identityconnectors.framework.common.objects.OperationOptionInfo;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationOptionsBuilder;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.PredefinedAttributes;
import org.identityconnectors.framework.common.objects.QualifiedUid;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.ScriptContext;
import org.identityconnectors.framework.common.objects.SearchResult;
import org.identityconnectors.framework.common.objects.SortKey;
import org.identityconnectors.framework.common.objects.SyncDelta;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.Filter;
import org.identityconnectors.framework.impl.api.APIConfigurationImpl;
import org.identityconnectors.framework.impl.api.local.LocalConnectorInfoImpl;
import org.identityconnectors.framework.impl.api.local.ObjectPool;
import org.identityconnectors.framework.impl.api.local.ObjectPool.Statistics;
import org.identityconnectors.framework.impl.api.local.operations.ConnectorOperationalContext;
import org.identityconnectors.framework.spi.Connector;
import org.identityconnectors.framework.spi.PoolableConnector;

import com.evolveum.midpoint.prism.crypto.Protector;
import com.evolveum.midpoint.prism.delta.ChangeType;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.delta.PropertyDelta;
import com.evolveum.midpoint.prism.match.DistinguishedNameMatchingRule;
import com.evolveum.midpoint.prism.match.StringIgnoreCaseMatchingRule;
import com.evolveum.midpoint.prism.match.UuidMatchingRule;
import com.evolveum.midpoint.prism.match.XmlMatchingRule;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.schema.PrismSchema;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.prism.xml.XsdTypeMapper;
import com.evolveum.midpoint.provisioning.ucf.api.AttributesToReturn;
import com.evolveum.midpoint.provisioning.ucf.api.Change;
import com.evolveum.midpoint.provisioning.ucf.api.ConnectorInstance;
import com.evolveum.midpoint.provisioning.ucf.api.ExecuteProvisioningScriptOperation;
import com.evolveum.midpoint.provisioning.ucf.api.ExecuteScriptArgument;
import com.evolveum.midpoint.provisioning.ucf.api.GenericFrameworkException;
import com.evolveum.midpoint.provisioning.ucf.api.Operation;
import com.evolveum.midpoint.provisioning.ucf.api.PasswordChangeOperation;
import com.evolveum.midpoint.provisioning.ucf.api.PropertyModificationOperation;
import com.evolveum.midpoint.provisioning.ucf.api.ShadowResultHandler;
import com.evolveum.midpoint.provisioning.ucf.impl.connid.query.FilterInterpreter;
import com.evolveum.midpoint.schema.CapabilityUtil;
import com.evolveum.midpoint.schema.constants.ConnectorTestOperation;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.internals.InternalCounters;
import com.evolveum.midpoint.schema.internals.InternalMonitor;
import com.evolveum.midpoint.schema.result.AsynchronousOperationResult;
import com.evolveum.midpoint.schema.result.AsynchronousOperationReturnValue;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.result.OperationResultStatus;
import com.evolveum.midpoint.schema.util.ActivationUtil;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.schema.util.ShadowUtil;
import com.evolveum.midpoint.task.api.StateReporter;
import com.evolveum.midpoint.schema.util.SchemaDebugUtil;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
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.ActivationStatusType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ActivationType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.BeforeAfterType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnectorType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.CredentialsType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.CriticalityType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.LockoutStatusType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PasswordType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ProvisioningScriptHostType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowKindType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationLockoutStatusCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationStatusCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationValidityCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.AuxiliaryObjectClassesCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.CapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.CreateCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.CredentialsCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.DeleteCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.LiveSyncCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.PasswordCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ReadCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.SchemaCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ScriptCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ScriptCapabilityType.Host;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.TestConnectionCapabilityType;
import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.UpdateCapabilityType;
import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType;

/**
 * Implementation of ConnectorInstance for ConnId connectors.
 * <p/>
 * This class implements the ConnectorInstance interface. The methods are
 * converting the data from the "midPoint semantics" as seen by the
 * ConnectorInstance interface to the "ConnId semantics" as seen by the ConnId
 * framework.
 *
 * @author Radovan Semancik
 */
public class ConnectorInstanceConnIdImpl implements ConnectorInstance {

    private static final com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ObjectFactory CAPABILITY_OBJECT_FACTORY = new com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ObjectFactory();

    private static final Trace LOGGER = TraceManager.getTrace(ConnectorInstanceConnIdImpl.class);

    ConnectorInfo cinfo;
    ConnectorType connectorType;
    ConnectorFacade connIdConnectorFacade;
    String resourceSchemaNamespace;
    private PrismSchema connectorSchema;
    private APIConfiguration apiConfig = null;

    Protector protector;
    PrismContext prismContext;
    private ConnIdNameMapper connIdNameMapper;
    private ConnIdConvertor connIdConvertor;

    private ResourceSchema resourceSchema = null;
    private Collection<Object> capabilities = null;
    private String description;
    private boolean caseIgnoreAttributeNames = false;
    private Boolean legacySchema = null;
    private boolean supportsReturnDefaultAttributes = false;

    ConnectorInstanceConnIdImpl(ConnectorInfo connectorInfo, ConnectorType connectorType, String schemaNamespace,
            PrismSchema connectorSchema, Protector protector, PrismContext prismContext) {
        this.cinfo = connectorInfo;
        this.connectorType = connectorType;
        this.resourceSchemaNamespace = schemaNamespace;
        this.connectorSchema = connectorSchema;
        this.protector = protector;
        this.prismContext = prismContext;
        connIdNameMapper = new ConnIdNameMapper(schemaNamespace);
        connIdConvertor = new ConnIdConvertor(protector, resourceSchemaNamespace);
        connIdConvertor.setIcfNameMapper(connIdNameMapper);
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getSchemaNamespace() {
        return resourceSchemaNamespace;
    }

    public void setResourceSchema(ResourceSchema resourceSchema) {
        this.resourceSchema = resourceSchema;
        connIdNameMapper.setResourceSchema(resourceSchema);
    }

    public void resetResourceSchema() {
        setResourceSchema(null);
    }

    @Override
    public void configure(PrismContainerValue<?> configurationOriginal, OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException, SchemaException, ConfigurationException {

        OperationResult result = parentResult.createSubresult(ConnectorInstance.OPERATION_CONFIGURE);

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Configuring connector {}, provided configuration:\n{}", connectorType,
                    configurationOriginal.debugDump(1));
        }

        try {
            // Get default configuration for the connector. This is important,
            // as it contains types of connector configuration properties.

            // Make sure that the proper configuration schema is applied. This
            // will cause that all the "raw" elements are parsed
            PrismContainerValue<?> configurationCloned = configurationOriginal.clone();
            configurationCloned.applyDefinition(getConfigurationContainerDefinition());

            ConnIdConfigurationTransformer configTransformer = new ConnIdConfigurationTransformer(connectorType,
                    cinfo, protector);
            // Transform XML configuration from the resource to the ConnId connector configuration
            try {
                apiConfig = configTransformer.transformConnectorConfiguration(configurationCloned);
            } catch (SchemaException e) {
                result.recordFatalError(e.getMessage(), e);
                throw e;
            }

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Configuring connector {}, transformed configuration:", connectorType);
                for (String propName : apiConfig.getConfigurationProperties().getPropertyNames()) {
                    LOGGER.trace("P: {} = {}", propName,
                            apiConfig.getConfigurationProperties().getProperty(propName).getValue());
                }
            }

            // Create new connector instance using the transformed configuration
            connIdConnectorFacade = ConnectorFacadeFactory.getInstance().newInstance(apiConfig);

            result.recordSuccess();

            PrismProperty<Boolean> legacySchemaConfigProperty = configurationCloned
                    .findProperty(new QName(SchemaConstants.NS_ICF_CONFIGURATION,
                            ConnectorFactoryConnIdImpl.CONNECTOR_SCHEMA_LEGACY_SCHEMA_XML_ELEMENT_NAME));
            if (legacySchemaConfigProperty != null) {
                legacySchema = legacySchemaConfigProperty.getRealValue();
            }
            LOGGER.trace("Legacy schema (config): {}", legacySchema);

        } catch (Throwable ex) {
            Throwable midpointEx = processConnIdException(ex, this, result);
            result.computeStatus("Removing attribute values failed");
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof ConfigurationException) {
                throw (ConfigurationException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }
    }

    private PrismContainerDefinition<?> getConfigurationContainerDefinition() throws SchemaException {
        QName configContainerQName = new QName(connectorType.getNamespace(),
                ResourceType.F_CONNECTOR_CONFIGURATION.getLocalPart());
        PrismContainerDefinition<?> configContainerDef = connectorSchema
                .findContainerDefinitionByElementName(configContainerQName);
        if (configContainerDef == null) {
            throw new SchemaException("No definition of container " + configContainerQName
                    + " in configuration schema for connector " + this);
        }
        return configContainerDef;
    }

    private QName icfAttributeInfoToMatchingRule(AttributeInfo attributeInfo) {
        String icfSubtype = attributeInfo.getSubtype();
        if (icfSubtype == null) {
            return null;
        }
        if (AttributeInfo.Subtypes.STRING_CASE_IGNORE.toString().equals(icfSubtype)) {
            return StringIgnoreCaseMatchingRule.NAME;
        }
        if (AttributeInfo.Subtypes.STRING_LDAP_DN.toString().equals(icfSubtype)) {
            return DistinguishedNameMatchingRule.NAME;
        }
        if (AttributeInfo.Subtypes.STRING_XML.toString().equals(icfSubtype)) {
            return XmlMatchingRule.NAME;
        }
        if (AttributeInfo.Subtypes.STRING_UUID.toString().equals(icfSubtype)) {
            return UuidMatchingRule.NAME;
        }
        LOGGER.debug("Unknown subtype {} defined for attribute {}, ignoring (no matching rule definition)",
                icfSubtype, attributeInfo.getName());
        return null;
    }

    @Override
    public ConnectorOperationalStatus getOperationalStatus() throws ObjectNotFoundException {

        if (!(cinfo instanceof LocalConnectorInfoImpl)) {
            LOGGER.trace("Cannot get operational status of a remote connector {}", connectorType);
            return null;
        }

        if (apiConfig == null) {
            LOGGER.trace("Cannot get operational status of a connector {}: connector not yet configured",
                    connectorType);
            throw new IllegalStateException("Connector " + connectorType + " not yet configured");
        }

        ConnectorOperationalStatus status = new ConnectorOperationalStatus();

        ConnectorOperationalContext connectorOperationalContext = new ConnectorOperationalContext(
                (LocalConnectorInfoImpl) cinfo, (APIConfigurationImpl) apiConfig);

        Class<? extends Connector> connectorClass = connectorOperationalContext.getConnectorClass();
        if (connectorClass != null) {
            status.setConnectorClassName(connectorClass.getName());
        }

        ObjectPoolConfiguration poolConfiguration = apiConfig.getConnectorPoolConfiguration();
        if (poolConfiguration != null) {
            status.setPoolConfigMaxSize(poolConfiguration.getMaxObjects());
            status.setPoolConfigMinIdle(poolConfiguration.getMinIdle());
            status.setPoolConfigMaxIdle(poolConfiguration.getMaxIdle());
            status.setPoolConfigWaitTimeout(poolConfiguration.getMaxWait());
            status.setPoolConfigMinEvictableIdleTime(poolConfiguration.getMinEvictableIdleTimeMillis());
        }

        ObjectPool<PoolableConnector> pool = connectorOperationalContext.getPool();
        if (pool != null) {
            Statistics poolStats = pool.getStatistics();
            if (poolStats != null) {
                status.setPoolStatusNumActive(poolStats.getNumActive());
                status.setPoolStatusNumIdle(poolStats.getNumIdle());
            }
        }

        return status;
    }

    @Override
    public void initialize(ResourceSchema resourceSchema, Collection<Object> capabilities,
            boolean caseIgnoreAttributeNames, OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException, ConfigurationException {

        // Result type for this operation
        OperationResult result = parentResult.createSubresult(ConnectorInstance.OPERATION_INITIALIZE);
        result.addContext("connector", connectorType);
        result.addContext(OperationResult.CONTEXT_IMPLEMENTATION_CLASS, ConnectorFactoryConnIdImpl.class);

        if (connIdConnectorFacade == null) {
            result.recordFatalError("Attempt to use unconfigured connector");
            throw new IllegalStateException(
                    "Attempt to use unconfigured connector " + ObjectTypeUtil.toShortString(connectorType));
        }

        setResourceSchema(resourceSchema);
        this.capabilities = capabilities;
        this.caseIgnoreAttributeNames = caseIgnoreAttributeNames;

        if (resourceSchema != null && legacySchema == null) {
            legacySchema = detectLegacySchema(resourceSchema);
        }

        if (resourceSchema == null || capabilities == null) {
            try {
                boolean supportsSchema = processOperationCapabilities(result);
                if (supportsSchema) {
                    retrieveResourceSchema(null, result);
                }
            } catch (CommunicationException ex) {
                // This is in fact fatal. There is not schema. Not even the pre-cached one.
                // The connector will not be able to work.
                result.recordFatalError(ex);
                throw ex;
            } catch (ConfigurationException ex) {
                result.recordFatalError(ex);
                throw ex;
            } catch (GenericFrameworkException ex) {
                result.recordFatalError(ex);
                throw ex;
            }
        }

        result.recordSuccess();
    }

    @Override
    public ResourceSchema fetchResourceSchema(List<QName> generateObjectClasses, OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException, ConfigurationException {

        // Result type for this operation
        OperationResult result = parentResult
                .createSubresult(ConnectorInstance.class.getName() + ".fetchResourceSchema");
        result.addContext("connector", connectorType);

        try {
            boolean supportsSchema = processOperationCapabilities(result);
            if (!supportsSchema) {
                result.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Connector does not support schema");
                LOGGER.trace("Connector instance {} does not support schema, skipping", this);
                return null;
            }
            retrieveResourceSchema(generateObjectClasses, result);
        } catch (CommunicationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (ConfigurationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (GenericFrameworkException ex) {
            result.recordFatalError(ex);
            throw ex;
        }

        if (resourceSchema == null) {
            result.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Connector does not support schema");
        } else {
            result.recordSuccess();
        }

        return resourceSchema;
    }

    @Override
    public Collection<Object> fetchCapabilities(OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException, ConfigurationException {

        // Result type for this operation
        OperationResult result = parentResult
                .createMinorSubresult(ConnectorInstance.class.getName() + ".fetchCapabilities");
        result.addContext("connector", connectorType);

        try {
            boolean supportsSchema = processOperationCapabilities(result);
            if (supportsSchema) {
                LOGGER.trace("Connector instance {} does not support schema, skipping", this);
                // we need to get schema to figure out all the capabilities
                retrieveResourceSchema(null, result);
            }
        } catch (CommunicationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (ConfigurationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (GenericFrameworkException ex) {
            result.recordFatalError(ex);
            throw ex;
        }

        result.recordSuccess();

        return capabilities;
    }

    private boolean processOperationCapabilities(OperationResult parentResult) {
        capabilities = new ArrayList<>();

        // Create capabilities from supported connector operations

        InternalMonitor.recordConnectorOperation("getSupportedOperations");
        Set<Class<? extends APIOperation>> supportedOperations = connIdConnectorFacade.getSupportedOperations();

        LOGGER.trace("Connector supported operations: {}", supportedOperations);

        boolean supportsSchema = false;
        if (supportedOperations.contains(SchemaApiOp.class)) {
            SchemaCapabilityType capSchema = new SchemaCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createSchema(capSchema));
            supportsSchema = true;
        }

        if (supportedOperations.contains(SyncApiOp.class)) {
            LiveSyncCapabilityType capSync = new LiveSyncCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createLiveSync(capSync));
        }

        if (supportedOperations.contains(TestApiOp.class)) {
            TestConnectionCapabilityType capTest = new TestConnectionCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createTestConnection(capTest));
        }

        if (supportedOperations.contains(CreateApiOp.class)) {
            CreateCapabilityType capCreate = new CreateCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createCreate(capCreate));
        }

        if (supportedOperations.contains(GetApiOp.class) || supportedOperations.contains(SearchApiOp.class)) {
            ReadCapabilityType capRead = new ReadCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createRead(capRead));
        }

        if (supportedOperations.contains(UpdateApiOp.class)) {
            UpdateCapabilityType capUpdate = new UpdateCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createUpdate(capUpdate));
        }

        if (supportedOperations.contains(DeleteApiOp.class)) {
            DeleteCapabilityType capDelete = new DeleteCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createDelete(capDelete));
        }

        if (supportedOperations.contains(ScriptOnResourceApiOp.class)
                || supportedOperations.contains(ScriptOnConnectorApiOp.class)) {
            ScriptCapabilityType capScript = new ScriptCapabilityType();
            if (supportedOperations.contains(ScriptOnResourceApiOp.class)) {
                Host host = new Host();
                host.setType(ProvisioningScriptHostType.RESOURCE);
                capScript.getHost().add(host);
                // language is unknown here
            }
            if (supportedOperations.contains(ScriptOnConnectorApiOp.class)) {
                Host host = new Host();
                host.setType(ProvisioningScriptHostType.CONNECTOR);
                capScript.getHost().add(host);
                // language is unknown here
            }
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createScript(capScript));
        }

        return supportsSchema;
    }

    private void retrieveResourceSchema(List<QName> generateObjectClasses, OperationResult parentResult)
            throws CommunicationException, ConfigurationException, GenericFrameworkException {
        // Connector operation cannot create result for itself, so we need to
        // create result for it
        OperationResult icfResult = parentResult.createSubresult(ConnectorFacade.class.getName() + ".schema");
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        org.identityconnectors.framework.common.objects.Schema icfSchema = null;
        try {

            // Fetch the schema from the connector (which actually gets that
            // from the resource).
            InternalMonitor.recordConnectorOperation("schema");
            // TODO have context present
            //recordIcfOperationStart(reporter, ProvisioningOperation.ICF_GET_SCHEMA, null);
            icfSchema = connIdConnectorFacade.schema();
            //recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET_SCHEMA, null);

            icfResult.recordSuccess();
        } catch (UnsupportedOperationException ex) {
            //recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET_SCHEMA, null, ex);
            // The connector does no support schema() operation.
            icfResult.recordStatus(OperationResultStatus.NOT_APPLICABLE, ex.getMessage());
            resetResourceSchema();
            return;
        } catch (Throwable ex) {
            //recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET_SCHEMA, null, ex);
            // conditions.
            // Therefore this kind of heavy artillery is necessary.
            // ICF interface does not specify exceptions or other error
            // TODO maybe we can try to catch at least some specific exceptions
            Throwable midpointEx = processConnIdException(ex, this, icfResult);

            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof ConfigurationException) {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw (ConfigurationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw (Error) midpointEx;
            } else {
                icfResult.recordFatalError(midpointEx.getMessage(), midpointEx);
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        if (icfSchema == null) {
            icfResult.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Null schema returned");
            resetResourceSchema();
            return;
        }

        parseResourceSchema(icfSchema, generateObjectClasses);
    }

    private void parseResourceSchema(org.identityconnectors.framework.common.objects.Schema icfSchema,
            List<QName> generateObjectClasses) {

        AttributeInfo passwordAttributeInfo = null;
        AttributeInfo enableAttributeInfo = null;
        AttributeInfo enableDateAttributeInfo = null;
        AttributeInfo disableDateAttributeInfo = null;
        AttributeInfo lockoutAttributeInfo = null;
        AttributeInfo auxiliaryObjectClasseAttributeInfo = null;

        // New instance of midPoint schema object
        setResourceSchema(new ResourceSchemaImpl(getSchemaNamespace(), prismContext));

        if (legacySchema == null) {
            legacySchema = detectLegacySchema(icfSchema);
        }
        LOGGER.trace("Converting resource schema (legacy mode: {})", legacySchema);
        LOGGER.trace("Generating object classes: {}", generateObjectClasses);

        Set<ObjectClassInfo> objectClassInfoSet = icfSchema.getObjectClassInfo();
        // Let's convert every objectclass in the ConnId schema ...
        for (ObjectClassInfo objectClassInfo : objectClassInfoSet) {

            // "Flat" ConnId object class names needs to be mapped to QNames
            QName objectClassXsdName = connIdNameMapper.objectClassToQname(
                    new ObjectClass(objectClassInfo.getType()), getSchemaNamespace(), legacySchema);

            if (!shouldBeGenerated(generateObjectClasses, objectClassXsdName)) {
                LOGGER.trace("Skipping object class {} ({})", objectClassInfo.getType(), objectClassXsdName);
                continue;
            }

            LOGGER.trace("Converting object class {} ({})", objectClassInfo.getType(), objectClassXsdName);

            // ResourceObjectDefinition is a midPpoint way how to represent an
            // object class.
            // The important thing here is the last "type" parameter
            // (objectClassXsdName). The rest is more-or-less cosmetics.
            ObjectClassComplexTypeDefinition ocDef = ((ResourceSchemaImpl) resourceSchema)
                    .createObjectClassDefinition(objectClassXsdName);

            // The __ACCOUNT__ objectclass in ConnId is a default account
            // objectclass. So mark it appropriately.
            if (ObjectClass.ACCOUNT_NAME.equals(objectClassInfo.getType())) {
                ((ObjectClassComplexTypeDefinitionImpl) ocDef).setKind(ShadowKindType.ACCOUNT);
                ((ObjectClassComplexTypeDefinitionImpl) ocDef).setDefaultInAKind(true);
            }

            ResourceAttributeDefinition<String> uidDefinition = null;
            ResourceAttributeDefinition<String> nameDefinition = null;
            boolean hasUidDefinition = false;

            int displayOrder = ConnectorFactoryConnIdImpl.ATTR_DISPLAY_ORDER_START;
            // Let's iterate over all attributes in this object class ...
            Set<AttributeInfo> attributeInfoSet = objectClassInfo.getAttributeInfo();
            for (AttributeInfo attributeInfo : attributeInfoSet) {
                String icfName = attributeInfo.getName();

                if (OperationalAttributes.PASSWORD_NAME.equals(icfName)) {
                    // This attribute will not go into the schema
                    // instead a "password" capability is used
                    passwordAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                if (OperationalAttributes.ENABLE_NAME.equals(icfName)) {
                    enableAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                if (OperationalAttributes.ENABLE_DATE_NAME.equals(icfName)) {
                    enableDateAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                if (OperationalAttributes.DISABLE_DATE_NAME.equals(icfName)) {
                    disableDateAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                if (OperationalAttributes.LOCK_OUT_NAME.equals(icfName)) {
                    lockoutAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                if (PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME.equals(icfName)) {
                    auxiliaryObjectClasseAttributeInfo = attributeInfo;
                    // Skip this attribute, capability is sufficient
                    continue;
                }

                String processedAttributeName = icfName;
                if ((Name.NAME.equals(icfName) || Uid.NAME.equals(icfName))
                        && attributeInfo.getNativeName() != null) {
                    processedAttributeName = attributeInfo.getNativeName();
                }

                QName attrXsdName = connIdNameMapper.convertAttributeNameToQName(processedAttributeName, ocDef);
                QName attrXsdType = ConnIdUtil.icfTypeToXsdType(attributeInfo.getType(), false);

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("  attr conversion ConnId: {}({}) -> XSD: {}({})", icfName,
                            attributeInfo.getType().getSimpleName(), PrettyPrinter.prettyPrint(attrXsdName),
                            PrettyPrinter.prettyPrint(attrXsdType));
                }

                // Create ResourceObjectAttributeDefinition, which is midPoint
                // way how to express attribute schema.
                ResourceAttributeDefinitionImpl attrDef = new ResourceAttributeDefinitionImpl(attrXsdName,
                        attrXsdType, prismContext);

                attrDef.setMatchingRuleQName(icfAttributeInfoToMatchingRule(attributeInfo));

                if (Name.NAME.equals(icfName)) {
                    nameDefinition = attrDef;
                    if (uidDefinition != null && attrXsdName.equals(uidDefinition.getName())) {
                        attrDef.setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_ORDER);
                        uidDefinition = attrDef;
                        hasUidDefinition = true;
                    } else {
                        if (attributeInfo.getNativeName() == null) {
                            // Set a better display name for __NAME__. The "name" is s very
                            // overloaded term, so let's try to make things
                            // a bit clearer
                            attrDef.setDisplayName(ConnectorFactoryConnIdImpl.ICFS_NAME_DISPLAY_NAME);
                        }
                        attrDef.setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_NAME_DISPLAY_ORDER);
                    }

                } else if (Uid.NAME.equals(icfName)) {
                    // UID can be the same as other attribute
                    ResourceAttributeDefinition existingDefinition = ocDef.findAttributeDefinition(attrXsdName);
                    if (existingDefinition != null) {
                        hasUidDefinition = true;
                        ((ResourceAttributeDefinitionImpl) existingDefinition)
                                .setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_ORDER);
                        uidDefinition = existingDefinition;
                        continue;
                    } else {
                        uidDefinition = attrDef;
                        if (attributeInfo.getNativeName() == null) {
                            attrDef.setDisplayName(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_NAME);
                        }
                        attrDef.setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_ORDER);
                    }

                } else {
                    // Check conflict with UID definition
                    if (uidDefinition != null && attrXsdName.equals(uidDefinition.getName())) {
                        attrDef.setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_ORDER);
                        uidDefinition = attrDef;
                        hasUidDefinition = true;
                    } else {
                        attrDef.setDisplayOrder(displayOrder);
                        displayOrder += ConnectorFactoryConnIdImpl.ATTR_DISPLAY_ORDER_INCREMENT;
                    }
                }

                attrDef.setNativeAttributeName(attributeInfo.getNativeName());
                attrDef.setFrameworkAttributeName(icfName);

                // Now we are going to process flags such as optional and
                // multi-valued
                Set<Flags> flagsSet = attributeInfo.getFlags();
                // System.out.println(flagsSet);

                attrDef.setMinOccurs(0);
                attrDef.setMaxOccurs(1);
                boolean canCreate = true;
                boolean canUpdate = true;
                boolean canRead = true;

                for (Flags flags : flagsSet) {
                    if (flags == Flags.REQUIRED) {
                        attrDef.setMinOccurs(1);
                    }
                    if (flags == Flags.MULTIVALUED) {
                        attrDef.setMaxOccurs(-1);
                    }
                    if (flags == Flags.NOT_CREATABLE) {
                        canCreate = false;
                    }
                    if (flags == Flags.NOT_READABLE) {
                        canRead = false;
                    }
                    if (flags == Flags.NOT_UPDATEABLE) {
                        canUpdate = false;
                    }
                    if (flags == Flags.NOT_RETURNED_BY_DEFAULT) {
                        attrDef.setReturnedByDefault(false);
                    }
                }

                attrDef.setCanAdd(canCreate);
                attrDef.setCanModify(canUpdate);
                attrDef.setCanRead(canRead);

                if (!Uid.NAME.equals(icfName)) {
                    ((ObjectClassComplexTypeDefinitionImpl) ocDef).add(attrDef);
                }
            }

            if (uidDefinition == null) {
                // Every object has UID in ConnId, therefore add a default definition if no other was specified
                uidDefinition = new ResourceAttributeDefinitionImpl<>(SchemaConstants.ICFS_UID, DOMUtil.XSD_STRING,
                        prismContext);
                // DO NOT make it mandatory. It must not be present on create hence it cannot be mandatory.
                ((ResourceAttributeDefinitionImpl) uidDefinition).setMinOccurs(0);
                ((ResourceAttributeDefinitionImpl) uidDefinition).setMaxOccurs(1);
                // Make it read-only
                ((ResourceAttributeDefinitionImpl) uidDefinition).setReadOnly();
                // Set a default display name
                ((ResourceAttributeDefinitionImpl) uidDefinition)
                        .setDisplayName(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_NAME);
                ((ResourceAttributeDefinitionImpl) uidDefinition)
                        .setDisplayOrder(ConnectorFactoryConnIdImpl.ICFS_UID_DISPLAY_ORDER);
                // Uid is a primary identifier of every object (this is the ConnId way)
            }
            if (!hasUidDefinition) {
                ((ObjectClassComplexTypeDefinitionImpl) ocDef).add(uidDefinition);
            }
            ((ObjectClassComplexTypeDefinitionImpl) ocDef).addPrimaryIdentifier(uidDefinition);
            if (uidDefinition != nameDefinition) {
                ((ObjectClassComplexTypeDefinitionImpl) ocDef).addSecondaryIdentifier(nameDefinition);
            }

            // Add schema annotations
            ((ObjectClassComplexTypeDefinitionImpl) ocDef).setNativeObjectClass(objectClassInfo.getType());
            ((ObjectClassComplexTypeDefinitionImpl) ocDef).setDisplayNameAttribute(nameDefinition.getName());
            ((ObjectClassComplexTypeDefinitionImpl) ocDef).setNamingAttribute(nameDefinition.getName());
            ((ObjectClassComplexTypeDefinitionImpl) ocDef).setAuxiliary(objectClassInfo.isAuxiliary());

            LOGGER.trace("  ... converted object class {}: {}", objectClassInfo.getType(), ocDef);
        }

        // This is the default for all resources.
        // (Currently there is no way how to obtain it from the connector.)
        // It can be disabled manually.
        AddRemoveAttributeValuesCapabilityType addRemove = new AddRemoveAttributeValuesCapabilityType();
        capabilities.add(CAPABILITY_OBJECT_FACTORY.createAddRemoveAttributeValues(addRemove));

        ActivationCapabilityType capAct = null;

        if (enableAttributeInfo != null) {
            if (capAct == null) {
                capAct = new ActivationCapabilityType();
            }
            ActivationStatusCapabilityType capActStatus = new ActivationStatusCapabilityType();
            capAct.setStatus(capActStatus);
            if (!enableAttributeInfo.isReturnedByDefault()) {
                capActStatus.setReturnedByDefault(false);
            }
        }

        if (enableDateAttributeInfo != null) {
            if (capAct == null) {
                capAct = new ActivationCapabilityType();
            }
            ActivationValidityCapabilityType capValidFrom = new ActivationValidityCapabilityType();
            capAct.setValidFrom(capValidFrom);
            if (!enableDateAttributeInfo.isReturnedByDefault()) {
                capValidFrom.setReturnedByDefault(false);
            }
        }

        if (disableDateAttributeInfo != null) {
            if (capAct == null) {
                capAct = new ActivationCapabilityType();
            }
            ActivationValidityCapabilityType capValidTo = new ActivationValidityCapabilityType();
            capAct.setValidTo(capValidTo);
            if (!disableDateAttributeInfo.isReturnedByDefault()) {
                capValidTo.setReturnedByDefault(false);
            }
        }

        if (lockoutAttributeInfo != null) {
            if (capAct == null) {
                capAct = new ActivationCapabilityType();
            }
            ActivationLockoutStatusCapabilityType capActStatus = new ActivationLockoutStatusCapabilityType();
            capAct.setLockoutStatus(capActStatus);
            if (!lockoutAttributeInfo.isReturnedByDefault()) {
                capActStatus.setReturnedByDefault(false);
            }
        }

        if (capAct != null) {
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createActivation(capAct));
        }

        if (passwordAttributeInfo != null) {
            CredentialsCapabilityType capCred = new CredentialsCapabilityType();
            PasswordCapabilityType capPass = new PasswordCapabilityType();
            if (!passwordAttributeInfo.isReturnedByDefault()) {
                capPass.setReturnedByDefault(false);
            }
            if (passwordAttributeInfo.isReadable()) {
                capPass.setReadable(true);
            }
            capCred.setPassword(capPass);
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createCredentials(capCred));
        }

        if (auxiliaryObjectClasseAttributeInfo != null) {
            AuxiliaryObjectClassesCapabilityType capAux = new AuxiliaryObjectClassesCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createAuxiliaryObjectClasses(capAux));
        }

        boolean canPageSize = false;
        boolean canPageOffset = false;
        boolean canSort = false;
        for (OperationOptionInfo searchOption : icfSchema.getSupportedOptionsByOperation(SearchApiOp.class)) {
            switch (searchOption.getName()) {
            case OperationOptions.OP_PAGE_SIZE:
                canPageSize = true;
                break;
            case OperationOptions.OP_PAGED_RESULTS_OFFSET:
                canPageOffset = true;
                break;
            case OperationOptions.OP_SORT_KEYS:
                canSort = true;
                break;
            case OperationOptions.OP_RETURN_DEFAULT_ATTRIBUTES:
                supportsReturnDefaultAttributes = true;
                break;
            }

        }

        if (canPageSize || canPageOffset || canSort) {
            PagedSearchCapabilityType capPage = new PagedSearchCapabilityType();
            capabilities.add(CAPABILITY_OBJECT_FACTORY.createPagedSearch(capPage));
        }

    }

    private boolean detectLegacySchema(Schema icfSchema) {
        Set<ObjectClassInfo> objectClassInfoSet = icfSchema.getObjectClassInfo();
        for (ObjectClassInfo objectClassInfo : objectClassInfoSet) {
            if (objectClassInfo.is(ObjectClass.ACCOUNT_NAME) || objectClassInfo.is(ObjectClass.GROUP_NAME)) {
                LOGGER.trace("This is legacy schema");
                return true;
            }
        }
        return false;
    }

    private boolean detectLegacySchema(ResourceSchema resourceSchema) {
        ComplexTypeDefinition accountObjectClass = resourceSchema.findComplexTypeDefinition(
                new QName(getSchemaNamespace(), SchemaConstants.ACCOUNT_OBJECT_CLASS_LOCAL_NAME));
        return accountObjectClass != null;
    }

    private boolean shouldBeGenerated(List<QName> generateObjectClasses, QName objectClassXsdName) {
        if (generateObjectClasses == null || generateObjectClasses.isEmpty()) {
            return true;
        }

        for (QName objClassToGenerate : generateObjectClasses) {
            if (objClassToGenerate.equals(objectClassXsdName)) {
                return true;
            }
        }

        return false;
    }

    private <C extends CapabilityType> C getCapability(Class<C> capClass) {
        if (capabilities == null) {
            return null;
        }
        for (Object cap : capabilities) {
            if (capClass.isAssignableFrom(cap.getClass())) {
                return (C) cap;
            }
        }
        return null;
    }

    @Override
    public PrismObject<ShadowType> fetchObject(ResourceObjectIdentification resourceObjectIdentification,
            AttributesToReturn attributesToReturn, StateReporter reporter, OperationResult parentResult)
            throws ObjectNotFoundException, CommunicationException, GenericFrameworkException, SchemaException,
            SecurityViolationException, ConfigurationException {

        Validate.notNull(resourceObjectIdentification, "Null primary identifiers");
        ObjectClassComplexTypeDefinition objectClassDefinition = resourceObjectIdentification
                .getObjectClassDefinition();
        // Result type for this operation
        OperationResult result = parentResult
                .createMinorSubresult(ConnectorInstance.class.getName() + ".fetchObject");
        result.addArbitraryObjectAsParam("resourceObjectDefinition", objectClassDefinition);
        result.addArbitraryObjectAsParam("identification", resourceObjectIdentification);
        result.addContext("connector", connectorType);

        if (connIdConnectorFacade == null) {
            result.recordFatalError("Attempt to use unconfigured connector");
            throw new IllegalStateException("Attempt to use unconfigured connector "
                    + ObjectTypeUtil.toShortString(connectorType) + " " + description);
        }

        // Get UID from the set of identifiers
        Uid uid;
        try {
            uid = getUid(resourceObjectIdentification);
        } catch (SchemaException e) {
            result.recordFatalError(e);
            throw e;
        }
        if (uid == null) {
            result.recordFatalError(
                    "Required attribute UID not found in identification set while attempting to fetch object identified by "
                            + resourceObjectIdentification + " from " + description);
            throw new IllegalArgumentException(
                    "Required attribute UID not found in identification set while attempting to fetch object identified by "
                            + resourceObjectIdentification + " from " + description);
        }

        ObjectClass icfObjectClass = connIdNameMapper.objectClassToIcf(objectClassDefinition, getSchemaNamespace(),
                connectorType, legacySchema);
        if (icfObjectClass == null) {
            result.recordFatalError("Unable to determine object class from QName "
                    + objectClassDefinition.getTypeName() + " while attempting to fetch object identified by "
                    + resourceObjectIdentification + " from " + description);
            throw new IllegalArgumentException("Unable to determine object class from QName "
                    + objectClassDefinition.getTypeName() + " while attempting to fetch object identified by "
                    + resourceObjectIdentification + " from " + description);
        }

        OperationOptionsBuilder optionsBuilder = new OperationOptionsBuilder();
        convertToIcfAttrsToGet(objectClassDefinition, attributesToReturn, optionsBuilder);
        optionsBuilder.setAllowPartialResults(true);
        OperationOptions options = optionsBuilder.build();

        ConnectorObject co = null;
        try {

            // Invoke the ConnId connector
            co = fetchConnectorObject(reporter, objectClassDefinition, icfObjectClass, uid, options, result);

        } catch (CommunicationException ex) {
            result.recordFatalError(ex);
            // This is fatal. No point in continuing. Just re-throw the
            // exception.
            throw ex;
        } catch (GenericFrameworkException ex) {
            result.recordFatalError(ex);
            // This is fatal. No point in continuing. Just re-throw the
            // exception.
            throw ex;
        } catch (ConfigurationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (SecurityViolationException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (ObjectNotFoundException ex) {
            result.recordFatalError("Object not found");
            throw new ObjectNotFoundException("Object identified by " + resourceObjectIdentification
                    + " (ConnId UID " + uid + "), objectClass " + objectClassDefinition.getTypeName()
                    + "  was not found in " + description);
        } catch (SchemaException ex) {
            result.recordFatalError(ex);
            throw ex;
        } catch (RuntimeException ex) {
            result.recordFatalError(ex);
            throw ex;
        }

        if (co == null) {
            result.recordFatalError("Object not found");
            throw new ObjectNotFoundException("Object identified by " + resourceObjectIdentification
                    + " (ConnId UID " + uid + "), objectClass " + objectClassDefinition.getTypeName()
                    + " was not in " + description);
        }

        PrismObjectDefinition<ShadowType> shadowDefinition = toShadowDefinition(objectClassDefinition);
        PrismObject<ShadowType> shadow = connIdConvertor.convertToResourceObject(co, shadowDefinition, false,
                caseIgnoreAttributeNames, legacySchema);

        result.recordSuccess();
        return shadow;

    }

    private PrismObjectDefinition<ShadowType> toShadowDefinition(
            ObjectClassComplexTypeDefinition objectClassDefinition) {
        ResourceAttributeContainerDefinition resourceAttributeContainerDefinition = objectClassDefinition
                .toResourceAttributeContainerDefinition(ShadowType.F_ATTRIBUTES);
        return resourceAttributeContainerDefinition.toShadowDefinition();
    }

    /**
     * Returns null if nothing is found.
     */
    private ConnectorObject fetchConnectorObject(StateReporter reporter,
            ObjectClassComplexTypeDefinition objectClassDefinition, ObjectClass icfObjectClass, Uid uid,
            OperationOptions options, OperationResult parentResult)
            throws ObjectNotFoundException, CommunicationException, GenericFrameworkException,
            SecurityViolationException, SchemaException, ConfigurationException {

        // Connector operation cannot create result for itself, so we need to
        // create result for it
        OperationResult icfResult = parentResult
                .createMinorSubresult(ConnectorFacade.class.getName() + ".getObject");
        icfResult.addArbitraryObjectAsParam("objectClass", icfObjectClass);
        icfResult.addParam("uid", uid.getUidValue());
        icfResult.addArbitraryObjectAsParam("options", options);
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Fetching connector object ObjectClass={}, UID={}, options={}",
                    new Object[] { icfObjectClass, uid, ConnIdUtil.dumpOptions(options) });
        }

        ConnectorObject co = null;
        try {

            // Invoke the ConnId connector
            InternalMonitor.recordConnectorOperation("getObject");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_GET, objectClassDefinition, uid);
            co = connIdConnectorFacade.getObject(icfObjectClass, uid, options);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET, objectClassDefinition, uid);

            icfResult.recordSuccess();
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET, objectClassDefinition, ex, uid);
            String desc = this.getHumanReadableName() + " while getting object identified by ConnId UID '"
                    + uid.getUidValue() + "'";
            Throwable midpointEx = processConnIdException(ex, desc, icfResult);
            icfResult.computeStatus("Add object failed");

            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                icfResult.muteError();
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof ConfigurationException) {
                throw (ConfigurationException) midpointEx;
            } else if (midpointEx instanceof SecurityViolationException) {
                throw (SecurityViolationException) midpointEx;
            } else if (midpointEx instanceof ObjectNotFoundException) {
                LOGGER.trace("Got ObjectNotFoundException while looking for resource object ConnId UID: {}", uid);
                return null;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                // This should not happen. But some connectors are very strange.
                throw new SystemException(
                        "ERROR: " + midpointEx.getClass().getName() + ": " + midpointEx.getMessage(), midpointEx);
            } else {
                throw new SystemException(midpointEx.getClass().getName() + ": " + midpointEx.getMessage(),
                        midpointEx);
            }

        }

        return co;
    }

    private void convertToIcfAttrsToGet(ObjectClassComplexTypeDefinition objectClassDefinition,
            AttributesToReturn attributesToReturn, OperationOptionsBuilder optionsBuilder) throws SchemaException {
        if (attributesToReturn == null) {
            return;
        }
        Collection<? extends ResourceAttributeDefinition> attrs = attributesToReturn.getAttributesToReturn();
        if (attributesToReturn.isReturnDefaultAttributes() && !attributesToReturn.isReturnPasswordExplicit()
                && (attrs == null || attrs.isEmpty())) {
            return;
        }
        List<String> icfAttrsToGet = new ArrayList<>();
        if (attributesToReturn.isReturnDefaultAttributes()) {
            if (supportsReturnDefaultAttributes) {
                optionsBuilder.setReturnDefaultAttributes(true);
            } else {
                // Add all the attributes that are defined as "returned by default" by the schema
                for (ResourceAttributeDefinition attributeDef : objectClassDefinition.getAttributeDefinitions()) {
                    if (attributeDef.isReturnedByDefault()) {
                        String attrName = connIdNameMapper.convertAttributeNameToIcf(attributeDef);
                        icfAttrsToGet.add(attrName);
                    }
                }
            }
        }
        if (attributesToReturn.isReturnPasswordExplicit()
                || (attributesToReturn.isReturnDefaultAttributes() && passwordReturnedByDefault())) {
            icfAttrsToGet.add(OperationalAttributes.PASSWORD_NAME);
        }
        if (attributesToReturn.isReturnAdministrativeStatusExplicit()
                || (attributesToReturn.isReturnDefaultAttributes() && enabledReturnedByDefault())) {
            icfAttrsToGet.add(OperationalAttributes.ENABLE_NAME);
        }
        if (attributesToReturn.isReturnLockoutStatusExplicit()
                || (attributesToReturn.isReturnDefaultAttributes() && lockoutReturnedByDefault())) {
            icfAttrsToGet.add(OperationalAttributes.LOCK_OUT_NAME);
        }
        if (attributesToReturn.isReturnValidFromExplicit()
                || (attributesToReturn.isReturnDefaultAttributes() && validFromReturnedByDefault())) {
            icfAttrsToGet.add(OperationalAttributes.ENABLE_DATE_NAME);
        }
        if (attributesToReturn.isReturnValidToExplicit()
                || (attributesToReturn.isReturnDefaultAttributes() && validToReturnedByDefault())) {
            icfAttrsToGet.add(OperationalAttributes.DISABLE_DATE_NAME);
        }

        if (attrs != null) {
            for (ResourceAttributeDefinition attrDef : attrs) {
                String attrName = connIdNameMapper.convertAttributeNameToIcf(attrDef);
                if (!icfAttrsToGet.contains(attrName)) {
                    icfAttrsToGet.add(attrName);
                }
            }
        }
        // Log full list here. ConnId is shortening it and it cannot be seen in logs.
        LOGGER.trace("Converted attributes ConnId attibutesToGet: {}", icfAttrsToGet);
        optionsBuilder.setAttributesToGet(icfAttrsToGet);
    }

    private boolean passwordReturnedByDefault() {
        CredentialsCapabilityType capability = CapabilityUtil.getCapability(capabilities,
                CredentialsCapabilityType.class);
        return CapabilityUtil.isPasswordReturnedByDefault(capability);
    }

    private boolean enabledReturnedByDefault() {
        ActivationCapabilityType capability = CapabilityUtil.getCapability(capabilities,
                ActivationCapabilityType.class);
        return CapabilityUtil.isActivationStatusReturnedByDefault(capability);
    }

    private boolean lockoutReturnedByDefault() {
        ActivationCapabilityType capability = CapabilityUtil.getCapability(capabilities,
                ActivationCapabilityType.class);
        return CapabilityUtil.isActivationLockoutStatusReturnedByDefault(capability);
    }

    private boolean validFromReturnedByDefault() {
        ActivationCapabilityType capability = CapabilityUtil.getCapability(capabilities,
                ActivationCapabilityType.class);
        return CapabilityUtil.isActivationValidFromReturnedByDefault(capability);
    }

    private boolean validToReturnedByDefault() {
        ActivationCapabilityType capability = CapabilityUtil.getCapability(capabilities,
                ActivationCapabilityType.class);
        return CapabilityUtil.isActivationValidToReturnedByDefault(capability);
    }

    @Override
    public AsynchronousOperationReturnValue<Collection<ResourceAttribute<?>>> addObject(
            PrismObject<? extends ShadowType> shadow, Collection<Operation> additionalOperations,
            StateReporter reporter, OperationResult parentResult) throws CommunicationException,
            GenericFrameworkException, SchemaException, ObjectAlreadyExistsException, ConfigurationException {
        validateShadow(shadow, "add", false);
        ShadowType shadowType = shadow.asObjectable();

        ResourceAttributeContainer attributesContainer = ShadowUtil.getAttributesContainer(shadow);
        OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".addObject");
        result.addParam("resourceObject", shadow);
        result.addParam("additionalOperations", DebugUtil.debugDump(additionalOperations)); // because of serialization issues

        ObjectClassComplexTypeDefinition ocDef;
        ResourceAttributeContainerDefinition attrContDef = attributesContainer.getDefinition();
        if (attrContDef != null) {
            ocDef = attrContDef.getComplexTypeDefinition();
        } else {
            ocDef = resourceSchema.findObjectClassDefinition(shadow.asObjectable().getObjectClass());
            if (ocDef == null) {
                throw new SchemaException("Unknown object class " + shadow.asObjectable().getObjectClass());
            }
        }

        // getting icf object class from resource object class
        ObjectClass icfObjectClass = connIdNameMapper.objectClassToIcf(shadow, getSchemaNamespace(), connectorType,
                BooleanUtils.isNotFalse(legacySchema));

        if (icfObjectClass == null) {
            result.recordFatalError("Couldn't get icf object class from " + shadow);
            throw new IllegalArgumentException("Couldn't get icf object class from " + shadow);
        }

        // setting ifc attributes from resource object attributes
        Set<Attribute> attributes = null;
        try {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("midPoint object before conversion:\n{}", attributesContainer.debugDump());
            }
            attributes = connIdConvertor.convertFromResourceObject(attributesContainer, ocDef);

            if (shadowType.getCredentials() != null && shadowType.getCredentials().getPassword() != null) {
                PasswordType password = shadowType.getCredentials().getPassword();
                ProtectedStringType protectedString = password.getValue();
                GuardedString guardedPassword = ConnIdUtil.toGuardedString(protectedString, "new password",
                        protector);
                if (guardedPassword != null) {
                    attributes.add(AttributeBuilder.build(OperationalAttributes.PASSWORD_NAME, guardedPassword));
                }
            }

            if (ActivationUtil.hasAdministrativeActivation(shadowType)) {
                attributes.add(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME,
                        ActivationUtil.isAdministrativeEnabled(shadowType)));
            }

            if (ActivationUtil.hasValidFrom(shadowType)) {
                attributes.add(AttributeBuilder.build(OperationalAttributes.ENABLE_DATE_NAME,
                        XmlTypeConverter.toMillis(shadowType.getActivation().getValidFrom())));
            }

            if (ActivationUtil.hasValidTo(shadowType)) {
                attributes.add(AttributeBuilder.build(OperationalAttributes.DISABLE_DATE_NAME,
                        XmlTypeConverter.toMillis(shadowType.getActivation().getValidTo())));
            }

            if (ActivationUtil.hasLockoutStatus(shadowType)) {
                attributes.add(AttributeBuilder.build(OperationalAttributes.LOCK_OUT_NAME,
                        ActivationUtil.isLockedOut(shadowType)));
            }

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("ConnId attributes after conversion:\n{}", ConnIdUtil.dump(attributes));
            }
        } catch (SchemaException | RuntimeException ex) {
            result.recordFatalError("Error while converting resource object attributes. Reason: " + ex.getMessage(),
                    ex);
            throw new SchemaException(
                    "Error while converting resource object attributes. Reason: " + ex.getMessage(), ex);
        }

        if (attributes == null) {
            result.recordFatalError("Couldn't set attributes for icf.");
            throw new IllegalStateException("Couldn't set attributes for icf.");
        }

        List<String> icfAuxiliaryObjectClasses = new ArrayList<>();
        for (QName auxiliaryObjectClass : shadowType.getAuxiliaryObjectClass()) {
            icfAuxiliaryObjectClasses.add(connIdNameMapper
                    .objectClassToIcf(auxiliaryObjectClass, resourceSchemaNamespace, connectorType, false)
                    .getObjectClassValue());
        }
        if (!icfAuxiliaryObjectClasses.isEmpty()) {
            AttributeBuilder ab = new AttributeBuilder();
            ab.setName(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME);
            ab.addValue(icfAuxiliaryObjectClasses);
            attributes.add(ab.build());
        }

        OperationOptionsBuilder operationOptionsBuilder = new OperationOptionsBuilder();
        OperationOptions options = operationOptionsBuilder.build();

        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.BEFORE, result);

        OperationResult connIdResult = result.createSubresult(ConnectorFacade.class.getName() + ".create");
        connIdResult.addArbitraryObjectAsParam("objectClass", icfObjectClass);
        connIdResult.addArbitraryObjectCollectionAsParam("auxiliaryObjectClasses", icfAuxiliaryObjectClasses);
        connIdResult.addArbitraryObjectCollectionAsParam("attributes", attributes);
        connIdResult.addArbitraryObjectAsParam("options", options);
        connIdResult.addContext("connector", connIdConnectorFacade.getClass());

        Uid uid = null;
        try {

            // CALL THE ConnId FRAMEWORK
            InternalMonitor.recordConnectorOperation("create");
            InternalMonitor.recordConnectorModification("create");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_CREATE, ocDef, null); // TODO provide object name
            uid = connIdConnectorFacade.create(icfObjectClass, attributes, options);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_CREATE, ocDef, uid);

        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_CREATE, ocDef, ex, null); // TODO name
            Throwable midpointEx = processConnIdException(ex, this, connIdResult);
            result.computeStatus("Add object failed");

            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof ObjectAlreadyExistsException) {
                throw (ObjectAlreadyExistsException) midpointEx;
            } else if (midpointEx instanceof CommunicationException) {
                //            icfResult.muteError();
                //            result.muteError();
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof ConfigurationException) {
                throw (ConfigurationException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        if (uid == null || uid.getUidValue() == null || uid.getUidValue().isEmpty()) {
            connIdResult.recordFatalError("ConnId did not returned UID after create");
            result.computeStatus("Add object failed");
            throw new GenericFrameworkException("ConnId did not returned UID after create");
        }

        Collection<ResourceAttribute<?>> identifiers = ConnIdUtil.convertToIdentifiers(uid,
                attributesContainer.getDefinition().getComplexTypeDefinition(), resourceSchema);
        for (ResourceAttribute<?> identifier : identifiers) {
            attributesContainer.getValue().addReplaceExisting(identifier);
        }
        connIdResult.recordSuccess();

        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.AFTER, result);

        result.computeStatus();
        return AsynchronousOperationReturnValue.wrap(attributesContainer.getAttributes(), result);
    }

    private void validateShadow(PrismObject<? extends ShadowType> shadow, String operation, boolean requireUid) {
        if (shadow == null) {
            throw new IllegalArgumentException("Cannot " + operation + " null " + shadow);
        }
        PrismContainer<?> attributesContainer = shadow.findContainer(ShadowType.F_ATTRIBUTES);
        if (attributesContainer == null) {
            throw new IllegalArgumentException("Cannot " + operation + " shadow without attributes container");
        }
        ResourceAttributeContainer resourceAttributesContainer = ShadowUtil.getAttributesContainer(shadow);
        if (resourceAttributesContainer == null) {
            throw new IllegalArgumentException("Cannot " + operation
                    + " shadow without attributes container of type ResourceAttributeContainer, got "
                    + attributesContainer.getClass());
        }
        if (requireUid) {
            Collection<ResourceAttribute<?>> identifiers = resourceAttributesContainer.getPrimaryIdentifiers();
            if (identifiers == null || identifiers.isEmpty()) {
                throw new IllegalArgumentException("Cannot " + operation + " shadow without identifiers");
            }
        }
    }

    // TODO [med] beware, this method does not obey its contract specified in the interface
    // (1) currently it does not return all the changes, only the 'side effect' changes
    // (2) it throws exceptions even if some of the changes were made
    // (3) among identifiers, only the UID value is updated on object rename
    //     (other identifiers are ignored on input and output of this method)

    @Override
    public AsynchronousOperationReturnValue<Collection<PropertyModificationOperation>> modifyObject(
            ObjectClassComplexTypeDefinition objectClassDef, PrismObject<ShadowType> shadow,
            Collection<? extends ResourceAttribute<?>> identifiers, Collection<Operation> changes,
            StateReporter reporter, OperationResult parentResult)
            throws ObjectNotFoundException, CommunicationException, GenericFrameworkException, SchemaException,
            SecurityViolationException, ObjectAlreadyExistsException {

        OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".modifyObject");
        result.addArbitraryObjectAsParam("objectClass", objectClassDef);
        result.addArbitraryObjectCollectionAsParam("identifiers", identifiers);
        result.addArbitraryObjectCollectionAsParam("changes", changes);

        if (changes.isEmpty()) {
            LOGGER.info("No modifications for connector object specified. Skipping processing.");
            result.recordNotApplicableIfUnknown();
            return AsynchronousOperationReturnValue.wrap(new ArrayList<PropertyModificationOperation>(0), result);
        }

        ObjectClass objClass = connIdNameMapper.objectClassToIcf(objectClassDef, getSchemaNamespace(),
                connectorType, legacySchema);

        Uid uid;
        try {
            uid = getUid(objectClassDef, identifiers);
        } catch (SchemaException e) {
            result.recordFatalError(e);
            throw e;
        }

        if (uid == null) {
            result.recordFatalError("Cannot detemine UID from identifiers: " + identifiers);
            throw new IllegalArgumentException("Cannot detemine UID from identifiers: " + identifiers);
        }
        String originalUid = uid.getUidValue();

        Set<Attribute> attributesToAdd = new HashSet<>();
        Set<Attribute> attributesToUpdate = new HashSet<>();
        Set<Attribute> attributesToRemove = new HashSet<>();

        Set<Operation> additionalOperations = new HashSet<>();
        PasswordChangeOperation passwordChangeOperation = null;
        Collection<PropertyDelta<?>> activationDeltas = new HashSet<>();
        PropertyDelta<ProtectedStringType> passwordDelta = null;
        PropertyDelta<QName> auxiliaryObjectClassDelta = null;

        for (Operation operation : changes) {
            if (operation == null) {
                IllegalArgumentException e = new IllegalArgumentException("Null operation in modifyObject");
                result.recordFatalError(e);
                throw e;
            }
            if (operation instanceof PropertyModificationOperation) {
                PropertyDelta<?> delta = ((PropertyModificationOperation) operation).getPropertyDelta();
                if (delta.getPath().equivalent(new ItemPath(ShadowType.F_AUXILIARY_OBJECT_CLASS))) {
                    auxiliaryObjectClassDelta = (PropertyDelta<QName>) delta;
                }
            }
        }

        try {
            ObjectClassComplexTypeDefinition structuralObjectClassDefinition = resourceSchema
                    .findObjectClassDefinition(objectClassDef.getTypeName());
            if (structuralObjectClassDefinition == null) {
                throw new SchemaException("No definition of structural object class " + objectClassDef.getTypeName()
                        + " in " + description);
            }
            Map<QName, ObjectClassComplexTypeDefinition> auxiliaryObjectClassMap = new HashMap<>();
            if (auxiliaryObjectClassDelta != null) {
                // Activation change means modification of attributes
                if (auxiliaryObjectClassDelta.isReplace()) {
                    if (auxiliaryObjectClassDelta.getValuesToReplace() == null
                            || auxiliaryObjectClassDelta.getValuesToReplace().isEmpty()) {
                        attributesToUpdate
                                .add(AttributeBuilder.build(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME));
                    } else {
                        addConvertedValues(auxiliaryObjectClassDelta.getValuesToReplace(), attributesToUpdate,
                                auxiliaryObjectClassMap);
                    }
                } else {
                    addConvertedValues(auxiliaryObjectClassDelta.getValuesToAdd(), attributesToAdd,
                            auxiliaryObjectClassMap);
                    addConvertedValues(auxiliaryObjectClassDelta.getValuesToDelete(), attributesToRemove,
                            auxiliaryObjectClassMap);
                }
            }

            for (Operation operation : changes) {
                if (operation instanceof PropertyModificationOperation) {
                    PropertyModificationOperation change = (PropertyModificationOperation) operation;
                    PropertyDelta<?> delta = change.getPropertyDelta();

                    if (delta.getParentPath().equivalent(new ItemPath(ShadowType.F_ATTRIBUTES))) {
                        if (delta.getDefinition() == null
                                || !(delta.getDefinition() instanceof ResourceAttributeDefinition)) {
                            ResourceAttributeDefinition def = objectClassDef
                                    .findAttributeDefinition(delta.getElementName());
                            if (def == null) {
                                String message = "No definition for attribute " + delta.getElementName()
                                        + " used in modification delta";
                                result.recordFatalError(message);
                                throw new SchemaException(message);
                            }
                            try {
                                delta.applyDefinition(def);
                            } catch (SchemaException e) {
                                result.recordFatalError(e.getMessage(), e);
                                throw e;
                            }
                        }
                        boolean isInRemovedAuxClass = false;
                        boolean isInAddedAuxClass = false;
                        ResourceAttributeDefinition<Object> structAttrDef = structuralObjectClassDefinition
                                .findAttributeDefinition(delta.getElementName());
                        // if this attribute is also in the structural object class. It does not matter if it is in
                        // aux object class, we cannot add/remove it with the object class unless it is normally requested
                        if (structAttrDef == null) {
                            if (auxiliaryObjectClassDelta != null && auxiliaryObjectClassDelta.isDelete()) {
                                // We need to change all the deltas of all the attributes that belong
                                // to the removed auxiliary object class from REPLACE to DELETE. The change of
                                // auxiliary object class and the change of the attributes must be done in
                                // one operation. Otherwise we get schema error. And as auxiliary object class
                                // is removed, the attributes must be removed as well.
                                for (PrismPropertyValue<QName> auxPval : auxiliaryObjectClassDelta
                                        .getValuesToDelete()) {
                                    ObjectClassComplexTypeDefinition auxDef = auxiliaryObjectClassMap
                                            .get(auxPval.getValue());
                                    ResourceAttributeDefinition<Object> attrDef = auxDef
                                            .findAttributeDefinition(delta.getElementName());
                                    if (attrDef != null) {
                                        isInRemovedAuxClass = true;
                                        break;
                                    }
                                }
                            }
                            if (auxiliaryObjectClassDelta != null && auxiliaryObjectClassDelta.isAdd()) {
                                // We need to change all the deltas of all the attributes that belong
                                // to the new auxiliary object class from REPLACE to ADD. The change of
                                // auxiliary object class and the change of the attributes must be done in
                                // one operation. Otherwise we get schema error. And as auxiliary object class
                                // is added, the attributes must be added as well.
                                for (PrismPropertyValue<QName> auxPval : auxiliaryObjectClassDelta
                                        .getValuesToAdd()) {
                                    ObjectClassComplexTypeDefinition auxOcDef = auxiliaryObjectClassMap
                                            .get(auxPval.getValue());
                                    ResourceAttributeDefinition<Object> auxAttrDef = auxOcDef
                                            .findAttributeDefinition(delta.getElementName());
                                    if (auxAttrDef != null) {
                                        isInAddedAuxClass = true;
                                        break;
                                    }
                                }
                            }
                        }
                        // Change in (ordinary) attributes. Transform to the ConnId attributes.
                        if (delta.isAdd()) {
                            ResourceAttribute<?> mpAttr = (ResourceAttribute<?>) delta.instantiateEmptyProperty();
                            mpAttr.addValues((Collection) PrismValue.cloneCollection(delta.getValuesToAdd()));
                            Attribute connIdAttr = connIdConvertor.convertToConnIdAttribute(mpAttr, objectClassDef);
                            if (mpAttr.getDefinition().isMultiValue()) {
                                attributesToAdd.add(connIdAttr);
                            } else {
                                // Force "update" for single-valued attributes instead of "add". This is saving one
                                // read in some cases. It should also make no substantial difference in such case.
                                // But it is working around some connector bugs.
                                attributesToUpdate.add(connIdAttr);
                            }
                        }
                        if (delta.isDelete()) {
                            ResourceAttribute<?> mpAttr = (ResourceAttribute<?>) delta.instantiateEmptyProperty();
                            if (mpAttr.getDefinition().isMultiValue() || isInRemovedAuxClass) {
                                mpAttr.addValues(
                                        (Collection) PrismValue.cloneCollection(delta.getValuesToDelete()));
                                Attribute connIdAttr = connIdConvertor.convertToConnIdAttribute(mpAttr,
                                        objectClassDef);
                                attributesToRemove.add(connIdAttr);
                            } else {
                                // Force "update" for single-valued attributes instead of "add". This is saving one
                                // read in some cases.
                                // Update attribute to no values. This will efficiently clean up the attribute.
                                // It should also make no substantial difference in such case.
                                // But it is working around some connector bugs.
                                Attribute connIdAttr = connIdConvertor.convertToConnIdAttribute(mpAttr,
                                        objectClassDef);
                                // update with EMTPY value. The mpAttr.addValues() is NOT in this branch
                                attributesToUpdate.add(connIdAttr);
                            }
                        }
                        if (delta.isReplace()) {
                            ResourceAttribute<?> mpAttr = (ResourceAttribute<?>) delta.instantiateEmptyProperty();
                            mpAttr.addValues((Collection) PrismValue.cloneCollection(delta.getValuesToReplace()));
                            Attribute connIdAttr = connIdConvertor.convertToConnIdAttribute(mpAttr, objectClassDef);
                            if (isInAddedAuxClass) {
                                attributesToAdd.add(connIdAttr);
                            } else {
                                attributesToUpdate.add(connIdAttr);
                            }
                        }
                    } else if (delta.getParentPath().equivalent(new ItemPath(ShadowType.F_ACTIVATION))) {
                        activationDeltas.add(delta);
                    } else if (delta.getParentPath().equivalent(
                            new ItemPath(new ItemPath(ShadowType.F_CREDENTIALS), CredentialsType.F_PASSWORD))) {
                        passwordDelta = (PropertyDelta<ProtectedStringType>) delta;
                    } else if (delta.getPath().equivalent(new ItemPath(ShadowType.F_AUXILIARY_OBJECT_CLASS))) {
                        // already processed
                    } else {
                        throw new SchemaException("Change of unknown attribute " + delta.getPath());
                    }

                } else if (operation instanceof PasswordChangeOperation) {
                    passwordChangeOperation = (PasswordChangeOperation) operation;
                    // TODO: check for multiple occurrences and fail

                } else if (operation instanceof ExecuteProvisioningScriptOperation) {
                    ExecuteProvisioningScriptOperation scriptOperation = (ExecuteProvisioningScriptOperation) operation;
                    additionalOperations.add(scriptOperation);

                } else {
                    throw new IllegalArgumentException(
                            "Unknown operation type " + operation.getClass().getName() + ": " + operation);
                }

            }
        } catch (SchemaException | RuntimeException e) {
            result.recordFatalError(e);
            throw e;
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("attributes:\nADD: {}\nUPDATE: {}\nREMOVE: {}", attributesToAdd, attributesToUpdate,
                    attributesToRemove);
        }

        // Needs three complete try-catch blocks because we need to create
        // icfResult for each operation
        // and handle the faults individually

        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.BEFORE, result);

        OperationResult connIdResult = null;
        try {
            if (!attributesToAdd.isEmpty()) {
                OperationOptions options = new OperationOptionsBuilder().build();
                connIdResult = result.createSubresult(ConnectorFacade.class.getName() + ".addAttributeValues");
                connIdResult.addArbitraryObjectAsParam("objectClass", objectClassDef);
                connIdResult.addParam("uid", uid.getUidValue());
                connIdResult.addArbitraryObjectAsParam("attributes", attributesToAdd);
                connIdResult.addArbitraryObjectAsParam("options", options);
                connIdResult.addContext("connector", connIdConnectorFacade.getClass());

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Invoking ConnId addAttributeValues(), objectclass={}, uid={}, attributes: {}",
                            new Object[] { objClass, uid, dumpAttributes(attributesToAdd) });
                }

                InternalMonitor.recordConnectorOperation("addAttributeValues");
                InternalMonitor.recordConnectorModification("addAttributeValues");

                // Invoking ConnId
                recordIcfOperationStart(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, uid);
                uid = connIdConnectorFacade.addAttributeValues(objClass, uid, attributesToAdd, options);
                recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, null, uid);

                connIdResult.recordSuccess();
            }
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, ex, uid);
            String desc = this.getHumanReadableName()
                    + " while adding attribute values to object identified by ConnId UID '" + uid.getUidValue()
                    + "'";
            Throwable midpointEx = processConnIdException(ex, desc, connIdResult);
            result.computeStatus("Adding attribute values failed");
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof ObjectNotFoundException) {
                throw (ObjectNotFoundException) midpointEx;
            } else if (midpointEx instanceof CommunicationException) {
                //in this situation this is not a critical error, becasue we know to handle it..so mute the error and sign it as expected
                result.muteError();
                connIdResult.muteError();
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof AlreadyExistsException) {
                throw (AlreadyExistsException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof SecurityViolationException) {
                throw (SecurityViolationException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        if (!attributesToUpdate.isEmpty() || activationDeltas != null || passwordDelta != null
                || auxiliaryObjectClassDelta != null) {

            try {
                if (activationDeltas != null) {
                    // Activation change means modification of attributes
                    convertFromActivation(attributesToUpdate, activationDeltas);
                }

                if (passwordDelta != null) {
                    // Activation change means modification of attributes
                    convertFromPassword(attributesToUpdate, passwordDelta);
                }

            } catch (SchemaException ex) {
                result.recordFatalError(
                        "Error while converting resource object attributes. Reason: " + ex.getMessage(), ex);
                throw new SchemaException(
                        "Error while converting resource object attributes. Reason: " + ex.getMessage(), ex);
            } catch (RuntimeException ex) {
                result.recordFatalError(
                        "Error while converting resource object attributes. Reason: " + ex.getMessage(), ex);
                throw ex;
            }

            if (!attributesToUpdate.isEmpty()) {
                OperationOptions options = new OperationOptionsBuilder().build();
                connIdResult = result.createSubresult(ConnectorFacade.class.getName() + ".update");
                connIdResult.addArbitraryObjectAsParam("objectClass", objectClassDef);
                connIdResult.addParam("uid", uid == null ? "null" : uid.getUidValue());
                connIdResult.addArbitraryObjectAsParam("attributes", attributesToUpdate);
                connIdResult.addArbitraryObjectAsParam("options", options);
                connIdResult.addContext("connector", connIdConnectorFacade.getClass());

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Invoking ConnId update(), objectclass={}, uid={}, attributes: {}",
                            new Object[] { objClass, uid, dumpAttributes(attributesToUpdate) });
                }

                try {
                    // Call ConnId
                    InternalMonitor.recordConnectorOperation("update");
                    InternalMonitor.recordConnectorModification("update");
                    recordIcfOperationStart(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, uid);
                    uid = connIdConnectorFacade.update(objClass, uid, attributesToUpdate, options);
                    recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, null, uid);

                    connIdResult.recordSuccess();
                } catch (Throwable ex) {
                    recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, ex, uid);
                    String desc = this.getHumanReadableName() + " while updating object identified by ConnId UID '"
                            + uid.getUidValue() + "'";
                    Throwable midpointEx = processConnIdException(ex, desc, connIdResult);
                    result.computeStatus("Update failed");
                    // Do some kind of acrobatics to do proper throwing of checked
                    // exception
                    if (midpointEx instanceof ObjectNotFoundException) {
                        throw (ObjectNotFoundException) midpointEx;
                    } else if (midpointEx instanceof CommunicationException) {
                        //in this situation this is not a critical error, becasue we know to handle it..so mute the error and sign it as expected
                        result.muteError();
                        connIdResult.muteError();
                        throw (CommunicationException) midpointEx;
                    } else if (midpointEx instanceof GenericFrameworkException) {
                        throw (GenericFrameworkException) midpointEx;
                    } else if (midpointEx instanceof SchemaException) {
                        throw (SchemaException) midpointEx;
                    } else if (midpointEx instanceof ObjectAlreadyExistsException) {
                        throw (ObjectAlreadyExistsException) midpointEx;
                    } else if (midpointEx instanceof RuntimeException) {
                        throw (RuntimeException) midpointEx;
                    } else if (midpointEx instanceof SecurityViolationException) {
                        throw (SecurityViolationException) midpointEx;
                    } else if (midpointEx instanceof Error) {
                        throw (Error) midpointEx;
                    } else {
                        throw new SystemException(
                                "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(),
                                ex);
                    }
                }
            }
        }

        try {
            if (!attributesToRemove.isEmpty()) {
                OperationOptions options = new OperationOptionsBuilder().build();
                connIdResult = result.createSubresult(ConnectorFacade.class.getName() + ".removeAttributeValues");
                connIdResult.addArbitraryObjectAsParam("objectClass", objectClassDef);
                connIdResult.addParam("uid", uid.getUidValue());
                connIdResult.addArbitraryObjectAsParam("attributes", attributesToRemove);
                connIdResult.addArbitraryObjectAsParam("options", options);
                connIdResult.addContext("connector", connIdConnectorFacade.getClass());

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Invoking ConnId removeAttributeValues(), objectclass={}, uid={}, attributes: {}",
                            new Object[] { objClass, uid, dumpAttributes(attributesToRemove) });
                }

                InternalMonitor.recordConnectorOperation("removeAttributeValues");
                InternalMonitor.recordConnectorModification("removeAttributeValues");
                recordIcfOperationStart(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, uid);
                uid = connIdConnectorFacade.removeAttributeValues(objClass, uid, attributesToRemove, options);
                recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, null, uid);
                connIdResult.recordSuccess();
            }
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_UPDATE, objectClassDef, ex, uid);
            String desc = this.getHumanReadableName()
                    + " while removing attribute values from object identified by ConnId UID '" + uid.getUidValue()
                    + "'";
            Throwable midpointEx = processConnIdException(ex, desc, connIdResult);
            result.computeStatus("Removing attribute values failed");
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof ObjectNotFoundException) {
                throw (ObjectNotFoundException) midpointEx;
            } else if (midpointEx instanceof CommunicationException) {
                //in this situation this is not a critical error, becasue we know to handle it..so mute the error and sign it as expected
                result.muteError();
                connIdResult.muteError();
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof ObjectAlreadyExistsException) {
                throw (ObjectAlreadyExistsException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof SecurityViolationException) {
                throw (SecurityViolationException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }
        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.AFTER, result);

        result.computeStatus();

        Collection<PropertyModificationOperation> sideEffectChanges = new ArrayList<>();
        if (!originalUid.equals(uid.getUidValue())) {
            // UID was changed during the operation, this is most likely a
            // rename
            PropertyDelta<String> uidDelta = createUidDelta(uid, getUidDefinition(objectClassDef, identifiers));
            PropertyModificationOperation uidMod = new PropertyModificationOperation(uidDelta);
            // TODO what about matchingRuleQName ?
            sideEffectChanges.add(uidMod);

            replaceUidValue(objectClassDef, identifiers, uid);
        }
        return AsynchronousOperationReturnValue.wrap(sideEffectChanges, result);
    }

    private PropertyDelta<String> createUidDelta(Uid uid, ResourceAttributeDefinition uidDefinition) {
        PropertyDelta<String> uidDelta = new PropertyDelta<String>(
                new ItemPath(ShadowType.F_ATTRIBUTES, uidDefinition.getName()), uidDefinition, prismContext);
        uidDelta.setValueToReplace(new PrismPropertyValue<>(uid.getUidValue()));
        return uidDelta;
    }

    private String dumpAttributes(Set<Attribute> attributes) {
        if (attributes == null) {
            return "(null)";
        }
        if (attributes.isEmpty()) {
            return "(empty)";
        }
        StringBuilder sb = new StringBuilder();
        for (Attribute attr : attributes) {
            sb.append("\n");
            if (attr.getValue() == null || attr.getValue().isEmpty()) {
                sb.append(attr.getName());
                sb.append(" (empty)");
            } else {
                for (Object value : attr.getValue()) {
                    sb.append(attr.getName());
                    sb.append(" = ");
                    sb.append(value);
                }
            }
        }
        return sb.toString();
    }

    @Override
    public AsynchronousOperationResult deleteObject(ObjectClassComplexTypeDefinition objectClass,
            Collection<Operation> additionalOperations, PrismObject<ShadowType> shadow,
            Collection<? extends ResourceAttribute<?>> identifiers, StateReporter reporter,
            OperationResult parentResult)
            throws ObjectNotFoundException, CommunicationException, GenericFrameworkException, SchemaException {
        Validate.notNull(objectClass, "No objectclass");

        OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".deleteObject");
        result.addArbitraryObjectCollectionAsParam("identifiers", identifiers);

        ObjectClass objClass = connIdNameMapper.objectClassToIcf(objectClass, getSchemaNamespace(), connectorType,
                legacySchema);
        Uid uid;
        try {
            uid = getUid(objectClass, identifiers);
        } catch (SchemaException e) {
            result.recordFatalError(e);
            throw e;
        }

        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.BEFORE, result);

        OperationResult icfResult = result.createSubresult(ConnectorFacade.class.getName() + ".delete");
        icfResult.addArbitraryObjectAsParam("uid", uid);
        icfResult.addArbitraryObjectAsParam("objectClass", objClass);
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        try {

            InternalMonitor.recordConnectorOperation("delete");
            InternalMonitor.recordConnectorModification("delete");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_DELETE, objectClass, uid);
            connIdConnectorFacade.delete(objClass, uid, new OperationOptionsBuilder().build());
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_DELETE, objectClass, null, uid);

            icfResult.recordSuccess();

        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_DELETE, objectClass, ex, uid);
            String desc = this.getHumanReadableName() + " while deleting object identified by ConnId UID '"
                    + uid.getUidValue() + "'";
            Throwable midpointEx = processConnIdException(ex, desc, icfResult);
            result.computeStatus("Removing attribute values failed");
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof ObjectNotFoundException) {
                throw (ObjectNotFoundException) midpointEx;
            } else if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                // Schema exception during delete? It must be a missing UID
                throw new IllegalArgumentException(midpointEx.getMessage(), midpointEx);
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        checkAndExecuteAdditionalOperations(reporter, additionalOperations, BeforeAfterType.AFTER, result);

        result.computeStatus();
        return AsynchronousOperationResult.wrap(result);
    }

    @Override
    public PrismProperty<?> deserializeToken(Object serializedToken) {
        return createTokenProperty(serializedToken);
    }

    @Override
    public <T> PrismProperty<T> fetchCurrentToken(ObjectClassComplexTypeDefinition objectClassDef,
            StateReporter reporter, OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException {

        OperationResult result = parentResult
                .createSubresult(ConnectorInstance.class.getName() + ".fetchCurrentToken");
        result.addArbitraryObjectAsParam("objectClass", objectClassDef);

        ObjectClass icfObjectClass;
        if (objectClassDef == null) {
            icfObjectClass = ObjectClass.ALL;
        } else {
            icfObjectClass = connIdNameMapper.objectClassToIcf(objectClassDef, getSchemaNamespace(), connectorType,
                    legacySchema);
        }

        OperationResult icfResult = result.createSubresult(ConnectorFacade.class.getName() + ".sync");
        icfResult.addContext("connector", connIdConnectorFacade.getClass());
        icfResult.addArbitraryObjectAsParam("icfObjectClass", icfObjectClass);

        SyncToken syncToken = null;
        try {
            InternalMonitor.recordConnectorOperation("getLatestSyncToken");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_GET_LATEST_SYNC_TOKEN, objectClassDef);
            syncToken = connIdConnectorFacade.getLatestSyncToken(icfObjectClass);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET_LATEST_SYNC_TOKEN, objectClassDef);
            icfResult.recordSuccess();
            icfResult.addReturn("syncToken", syncToken == null ? null : String.valueOf(syncToken.getValue()));
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_GET_LATEST_SYNC_TOKEN, objectClassDef, ex);
            Throwable midpointEx = processConnIdException(ex, this, icfResult);
            result.computeStatus();
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        if (syncToken == null) {
            result.recordWarning("Resource have not provided a current sync token");
            return null;
        }

        PrismProperty<T> property = getToken(syncToken);
        result.recordSuccess();
        return property;
    }

    @Override
    public List<Change> fetchChanges(ObjectClassComplexTypeDefinition objectClass, PrismProperty<?> lastToken,
            AttributesToReturn attrsToReturn, StateReporter reporter, OperationResult parentResult)
            throws CommunicationException, GenericFrameworkException, SchemaException, ConfigurationException {

        OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".fetchChanges");
        result.addArbitraryObjectAsContext("objectClass", objectClass);
        result.addArbitraryObjectAsParam("lastToken", lastToken);

        // create sync token from the property last token
        SyncToken syncToken = null;
        try {
            syncToken = getSyncToken(lastToken);
            LOGGER.trace("Sync token created from the property last token: {}",
                    syncToken == null ? null : syncToken.getValue());
        } catch (SchemaException ex) {
            result.recordFatalError(ex.getMessage(), ex);
            throw new SchemaException(ex.getMessage(), ex);
        }

        final List<SyncDelta> syncDeltas = new ArrayList<>();
        // get icf object class
        ObjectClass icfObjectClass;
        if (objectClass == null) {
            icfObjectClass = ObjectClass.ALL;
        } else {
            icfObjectClass = connIdNameMapper.objectClassToIcf(objectClass, getSchemaNamespace(), connectorType,
                    legacySchema);
        }

        OperationOptionsBuilder optionsBuilder = new OperationOptionsBuilder();
        if (objectClass != null) {
            convertToIcfAttrsToGet(objectClass, attrsToReturn, optionsBuilder);
        }
        OperationOptions options = optionsBuilder.build();

        SyncResultsHandler syncHandler = new SyncResultsHandler() {
            @Override
            public boolean handle(SyncDelta delta) {
                LOGGER.trace("Detected sync delta: {}", delta);
                return syncDeltas.add(delta);
            }
        };

        OperationResult connIdResult = result.createSubresult(ConnectorFacade.class.getName() + ".sync");
        connIdResult.addContext("connector", connIdConnectorFacade.getClass());
        connIdResult.addArbitraryObjectAsParam("connIdObjectClass", icfObjectClass);
        connIdResult.addArbitraryObjectAsParam("syncToken", syncToken);
        connIdResult.addArbitraryObjectAsParam("syncHandler", syncHandler);

        SyncToken lastReceivedToken;
        try {
            InternalMonitor.recordConnectorOperation("sync");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_SYNC, objectClass);
            lastReceivedToken = connIdConnectorFacade.sync(icfObjectClass, syncToken, syncHandler, options);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SYNC, objectClass);
            connIdResult.recordSuccess();
            connIdResult.addReturn(OperationResult.RETURN_COUNT, syncDeltas.size());
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SYNC, objectClass, ex);
            Throwable midpointEx = processConnIdException(ex, this, connIdResult);
            result.computeStatus();
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }
        // convert changes from icf to midpoint Change
        List<Change> changeList;
        try {
            changeList = getChangesFromSyncDeltas(icfObjectClass, syncDeltas, resourceSchema, result);
        } catch (SchemaException ex) {
            result.recordFatalError(ex.getMessage(), ex);
            throw new SchemaException(ex.getMessage(), ex);
        }

        if (lastReceivedToken != null) {
            Change lastChange = new Change((ObjectDelta) null, getToken(lastReceivedToken));
            LOGGER.trace("Adding last change: {}", lastChange);
            changeList.add(lastChange);
        }

        result.recordSuccess();
        result.addReturn(OperationResult.RETURN_COUNT, changeList == null ? 0 : changeList.size());
        return changeList;
    }

    @Override
    public void test(OperationResult parentResult) {

        OperationResult connectionResult = parentResult
                .createSubresult(ConnectorTestOperation.CONNECTOR_CONNECTION.getOperation());
        connectionResult.addContext(OperationResult.CONTEXT_IMPLEMENTATION_CLASS,
                ConnectorInstanceConnIdImpl.class);
        connectionResult.addContext("connector", connectorType);

        try {
            InternalMonitor.recordConnectorOperation("test");
            connIdConnectorFacade.test();
            connectionResult.recordSuccess();
        } catch (UnsupportedOperationException ex) {
            // Connector does not support test connection.
            connectionResult.recordStatus(OperationResultStatus.NOT_APPLICABLE,
                    "Operation not supported by the connector", ex);
            // Do not rethrow. Recording the status is just OK.
        } catch (Throwable icfEx) {
            Throwable midPointEx = processConnIdException(icfEx, this, connectionResult);
            connectionResult.recordFatalError(midPointEx);
        }
    }

    @Override
    public SearchResultMetadata search(final ObjectClassComplexTypeDefinition objectClassDefinition,
            final ObjectQuery query, final ShadowResultHandler handler, AttributesToReturn attributesToReturn,
            PagedSearchCapabilityType pagedSearchCapabilityType,
            SearchHierarchyConstraints searchHierarchyConstraints, final StateReporter reporter,
            OperationResult parentResult) throws CommunicationException, GenericFrameworkException,
            SecurityViolationException, SchemaException, ObjectNotFoundException {

        // Result type for this operation
        final OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".search");
        result.addArbitraryObjectAsParam("objectClass", objectClassDefinition);
        result.addContext("connector", connectorType);

        if (objectClassDefinition == null) {
            result.recordFatalError("Object class not defined");
            throw new IllegalArgumentException("objectClass not defined");
        }

        ObjectClass icfObjectClass = connIdNameMapper.objectClassToIcf(objectClassDefinition, getSchemaNamespace(),
                connectorType, legacySchema);
        if (icfObjectClass == null) {
            IllegalArgumentException ex = new IllegalArgumentException(
                    "Unable to determine object class from QName " + objectClassDefinition
                            + " while attempting to search objects by "
                            + ObjectTypeUtil.toShortString(connectorType));
            result.recordFatalError("Unable to determine object class", ex);
            throw ex;
        }
        final PrismObjectDefinition<ShadowType> objectDefinition = toShadowDefinition(objectClassDefinition);

        if (pagedSearchCapabilityType == null) {
            pagedSearchCapabilityType = getCapability(PagedSearchCapabilityType.class);
        }

        final boolean useConnectorPaging = pagedSearchCapabilityType != null;
        if (!useConnectorPaging && query != null && query.getPaging() != null
                && (query.getPaging().getOffset() != null || query.getPaging().getMaxSize() != null)) {
            InternalMonitor.recordCount(InternalCounters.CONNECTOR_SIMULATED_PAGING_SEARCH_COUNT);
        }

        final Holder<Integer> countHolder = new Holder<>(0);

        ResultsHandler icfHandler = new ResultsHandler() {
            @Override
            public boolean handle(ConnectorObject connectorObject) {
                // Convert ConnId-specific connector object to a generic
                // ResourceObject
                recordIcfOperationSuspend(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);
                int count = countHolder.getValue();
                countHolder.setValue(count + 1);
                if (!useConnectorPaging) {
                    // TODO allow offset or maxSize be null
                    if (query != null && query.getPaging() != null && query.getPaging().getOffset() != null
                            && query.getPaging().getMaxSize() != null) {
                        if (count < query.getPaging().getOffset()) {
                            recordResume();
                            return true;
                        }

                        if (count == (query.getPaging().getOffset() + query.getPaging().getMaxSize())) {
                            recordResume();
                            return false;
                        }
                    }
                }
                PrismObject<ShadowType> resourceObject;
                Validate.notNull(connectorObject, "null connector object");
                try {
                    resourceObject = connIdConvertor.convertToResourceObject(connectorObject, objectDefinition,
                            false, caseIgnoreAttributeNames, legacySchema);
                } catch (SchemaException e) {
                    recordResume();
                    throw new IntermediateException(e);
                }

                Validate.notNull(resourceObject, "null resource object");

                // .. and pass it to the handler
                boolean cont = handler.handle(resourceObject);
                if (!cont) {
                    result.recordWarning("Stopped on request from the handler");
                }
                recordResume();
                return cont;
            }

            private void recordResume() {
                recordIcfOperationResume(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);
            }
        };

        OperationOptionsBuilder optionsBuilder = new OperationOptionsBuilder();

        try {
            convertToIcfAttrsToGet(objectClassDefinition, attributesToReturn, optionsBuilder);
            if (query != null && query.isAllowPartialResults()) {
                optionsBuilder.setAllowPartialResults(query.isAllowPartialResults());
            }
            // preparing paging-related options
            if (useConnectorPaging && query != null && query.getPaging() != null) {
                ObjectPaging paging = query.getPaging();
                if (paging.getOffset() != null) {
                    optionsBuilder.setPagedResultsOffset(paging.getOffset() + 1); // ConnId API says the numbering starts at 1
                }
                if (paging.getMaxSize() != null) {
                    optionsBuilder.setPageSize(paging.getMaxSize());
                }
                QName orderByAttributeName;
                boolean isAscending;
                ItemPath orderByPath = paging.getOrderBy();
                String desc;
                if (orderByPath != null && !orderByPath.isEmpty()) {
                    orderByAttributeName = ShadowUtil.getAttributeName(orderByPath, "OrderBy path");
                    if (SchemaConstants.C_NAME.equals(orderByAttributeName)) {
                        orderByAttributeName = SchemaConstants.ICFS_NAME;
                    }
                    isAscending = paging.getDirection() != OrderDirection.DESCENDING;
                    desc = "(explicitly specified orderBy attribute)";
                } else {
                    orderByAttributeName = pagedSearchCapabilityType.getDefaultSortField();
                    isAscending = pagedSearchCapabilityType
                            .getDefaultSortDirection() != OrderDirectionType.DESCENDING;
                    desc = "(default orderBy attribute from capability definition)";
                }
                if (orderByAttributeName != null) {
                    String orderByIcfName = connIdNameMapper.convertAttributeNameToIcf(orderByAttributeName,
                            objectClassDefinition, desc);
                    optionsBuilder.setSortKeys(new SortKey(orderByIcfName, isAscending));
                }
            }
            if (searchHierarchyConstraints != null) {
                ResourceObjectIdentification baseContextIdentification = searchHierarchyConstraints
                        .getBaseContext();
                // Only LDAP connector really supports base context. And this one will work better with
                // DN. And DN is secondary identifier (__NAME__). This is ugly, but practical. It works around ConnId problems.
                ResourceAttribute<?> secondaryIdentifier = baseContextIdentification.getSecondaryIdentifier();
                if (secondaryIdentifier == null) {
                    SchemaException e = new SchemaException(
                            "No secondary identifier in base context identification " + baseContextIdentification);
                    result.recordFatalError(e);
                    throw e;
                }
                String secondaryIdentifierValue = secondaryIdentifier.getRealValue(String.class);
                ObjectClass baseContextIcfObjectClass = connIdNameMapper.objectClassToIcf(
                        baseContextIdentification.getObjectClassDefinition(), getSchemaNamespace(), connectorType,
                        legacySchema);
                QualifiedUid containerQualifiedUid = new QualifiedUid(baseContextIcfObjectClass,
                        new Uid(secondaryIdentifierValue));
                optionsBuilder.setContainer(containerQualifiedUid);
            }

        } catch (SchemaException e) {
            result.recordFatalError(e);
            throw e;
        }

        // Relax completeness requirements. This is a search, not get. So it is OK to
        // return incomplete member lists and similar attributes.
        optionsBuilder.setAllowPartialAttributeValues(true);

        OperationOptions options = optionsBuilder.build();

        Filter filter;
        try {
            filter = convertFilterToIcf(query, objectClassDefinition);
        } catch (SchemaException | RuntimeException e) {
            result.recordFatalError(e);
            throw e;
        }

        // Connector operation cannot create result for itself, so we need to
        // create result for it
        OperationResult icfResult = result.createSubresult(ConnectorFacade.class.getName() + ".search");
        icfResult.addArbitraryObjectAsParam("objectClass", icfObjectClass);
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        SearchResult icfSearchResult;
        try {

            InternalMonitor.recordConnectorOperation("search");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);
            icfSearchResult = connIdConnectorFacade.search(icfObjectClass, filter, icfHandler, options);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);

            icfResult.recordSuccess();
        } catch (IntermediateException inex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition, inex);
            SchemaException ex = (SchemaException) inex.getCause();
            icfResult.recordFatalError(ex);
            result.recordFatalError(ex);
            throw ex;
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition, ex);
            Throwable midpointEx = processConnIdException(ex, this, icfResult);
            result.computeStatus();
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof ObjectNotFoundException) {
                throw (ObjectNotFoundException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof SecurityViolationException) {
                throw (SecurityViolationException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        SearchResultMetadata metadata = null;
        if (icfSearchResult != null) {
            metadata = new SearchResultMetadata();
            metadata.setPagingCookie(icfSearchResult.getPagedResultsCookie());
            if (icfSearchResult.getRemainingPagedResults() >= 0) {
                metadata.setApproxNumberOfAllResults(icfSearchResult.getRemainingPagedResults());
            }
            if (!icfSearchResult.isAllResultsReturned()) {
                metadata.setPartialResults(true);
            }
        }

        if (result.isUnknown()) {
            result.recordSuccess();
        }

        return metadata;
    }

    @Override
    public int count(ObjectClassComplexTypeDefinition objectClassDefinition, final ObjectQuery query,
            PagedSearchCapabilityType pagedSearchCapabilityType, StateReporter reporter,
            OperationResult parentResult) throws CommunicationException, GenericFrameworkException, SchemaException,
            UnsupportedOperationException {

        // Result type for this operation
        final OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".count");
        result.addArbitraryObjectAsParam("objectClass", objectClassDefinition);
        result.addContext("connector", connectorType);

        if (objectClassDefinition == null) {
            result.recordFatalError("Object class not defined");
            throw new IllegalArgumentException("objectClass not defined");
        }

        ObjectClass icfObjectClass = connIdNameMapper.objectClassToIcf(objectClassDefinition, getSchemaNamespace(),
                connectorType, legacySchema);
        if (icfObjectClass == null) {
            IllegalArgumentException ex = new IllegalArgumentException(
                    "Unable to determine object class from QName " + objectClassDefinition
                            + " while attempting to search objects by "
                            + ObjectTypeUtil.toShortString(connectorType));
            result.recordFatalError("Unable to determine object class", ex);
            throw ex;
        }
        final boolean useConnectorPaging = pagedSearchCapabilityType != null;
        if (!useConnectorPaging) {
            throw new UnsupportedOperationException(
                    "ConnectorInstanceIcfImpl.count operation is supported only in combination with connector-implemented paging");
        }

        OperationOptionsBuilder optionsBuilder = new OperationOptionsBuilder();
        optionsBuilder.setAttributesToGet(Name.NAME);
        optionsBuilder.setPagedResultsOffset(1);
        optionsBuilder.setPageSize(1);
        if (pagedSearchCapabilityType.getDefaultSortField() != null) {
            String orderByIcfName = connIdNameMapper.convertAttributeNameToIcf(
                    pagedSearchCapabilityType.getDefaultSortField(), objectClassDefinition,
                    "(default sorting field)");
            boolean isAscending = pagedSearchCapabilityType
                    .getDefaultSortDirection() != OrderDirectionType.DESCENDING;
            optionsBuilder.setSortKeys(new SortKey(orderByIcfName, isAscending));
        }
        OperationOptions options = optionsBuilder.build();

        // Connector operation cannot create result for itself, so we need to
        // create result for it
        OperationResult icfResult = result.createSubresult(ConnectorFacade.class.getName() + ".search");
        icfResult.addArbitraryObjectAsParam("objectClass", icfObjectClass);
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        int retval;

        try {

            Filter filter = convertFilterToIcf(query, objectClassDefinition);
            final Holder<Integer> fetched = new Holder<>(0);

            ResultsHandler icfHandler = new ResultsHandler() {
                @Override
                public boolean handle(ConnectorObject connectorObject) {
                    fetched.setValue(fetched.getValue() + 1); // actually, this should execute at most once
                    return false;
                }
            };
            InternalMonitor.recordConnectorOperation("search");
            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);
            SearchResult searchResult = connIdConnectorFacade.search(icfObjectClass, filter, icfHandler, options);
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition);

            if (searchResult == null || searchResult.getRemainingPagedResults() == -1) {
                throw new UnsupportedOperationException(
                        "Connector does not seem to support paged searches or does not provide object count information");
            } else {
                retval = fetched.getValue() + searchResult.getRemainingPagedResults();
            }

            icfResult.recordSuccess();
        } catch (IntermediateException inex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition, inex);
            SchemaException ex = (SchemaException) inex.getCause();
            icfResult.recordFatalError(ex);
            result.recordFatalError(ex);
            throw ex;
        } catch (UnsupportedOperationException uoe) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition, uoe);
            icfResult.recordFatalError(uoe);
            result.recordFatalError(uoe);
            throw uoe;
        } catch (Throwable ex) {
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SEARCH, objectClassDefinition, ex);
            Throwable midpointEx = processConnIdException(ex, this, icfResult);
            result.computeStatus();
            // Do some kind of acrobatics to do proper throwing of checked
            // exception
            if (midpointEx instanceof CommunicationException) {
                throw (CommunicationException) midpointEx;
            } else if (midpointEx instanceof GenericFrameworkException) {
                throw (GenericFrameworkException) midpointEx;
            } else if (midpointEx instanceof SchemaException) {
                throw (SchemaException) midpointEx;
            } else if (midpointEx instanceof RuntimeException) {
                throw (RuntimeException) midpointEx;
            } else if (midpointEx instanceof Error) {
                throw (Error) midpointEx;
            } else {
                throw new SystemException(
                        "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
        }

        if (result.isUnknown()) {
            result.recordSuccess();
        }

        return retval;
    }

    private Filter convertFilterToIcf(ObjectQuery query, ObjectClassComplexTypeDefinition objectClassDefinition)
            throws SchemaException {
        Filter filter = null;
        if (query != null && query.getFilter() != null) {
            FilterInterpreter interpreter = new FilterInterpreter(objectClassDefinition);
            LOGGER.trace("Start to convert filter: {}", query.getFilter().debugDump());
            filter = interpreter.interpret(query.getFilter(), connIdNameMapper);

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("ConnId filter: {}", ConnIdUtil.dump(filter));
            }
        }
        return filter;
    }

    // UTILITY METHODS

    private Uid getUid(ResourceObjectIdentification resourceObjectIdentification) throws SchemaException {
        ResourceAttribute<String> primaryIdentifier = resourceObjectIdentification.getPrimaryIdentifier();
        if (primaryIdentifier == null) {
            return null;
        }
        String uidValue = primaryIdentifier.getRealValue();
        String nameValue = null;
        Collection<? extends ResourceAttribute<?>> secondaryIdentifiers = resourceObjectIdentification
                .getSecondaryIdentifiers();
        if (secondaryIdentifiers != null && secondaryIdentifiers.size() == 1) {
            nameValue = (String) secondaryIdentifiers.iterator().next().getRealValue();
        }
        if (uidValue != null) {
            if (nameValue == null) {
                return new Uid(uidValue);
            } else {
                return new Uid(uidValue, new Name(nameValue));
            }
        }
        return null;
    }

    /**
     * Looks up ConnId Uid identifier in a (potentially multi-valued) set of
     * identifiers. Handy method to convert midPoint identifier style to an ConnId
     * identifier style.
     *
     * @param identifiers
     *            midPoint resource object identifiers
     * @return ConnId UID or null
     */
    private Uid getUid(ObjectClassComplexTypeDefinition objectClass,
            Collection<? extends ResourceAttribute<?>> identifiers) throws SchemaException {
        if (identifiers.size() == 0) {
            return null;
        }
        if (identifiers.size() == 1) {
            try {
                return new Uid((String) identifiers.iterator().next().getRealValue());
            } catch (IllegalArgumentException e) {
                throw new SchemaException(e.getMessage(), e);
            }
        }
        String uidValue = null;
        String nameValue = null;
        for (ResourceAttribute<?> attr : identifiers) {
            if (objectClass.isPrimaryIdentifier(attr.getElementName())) {
                uidValue = ((ResourceAttribute<String>) attr).getValue().getValue();
            }
            if (objectClass.isSecondaryIdentifier(attr.getElementName())) {
                ResourceAttributeDefinition<?> attrDef = objectClass.findAttributeDefinition(attr.getElementName());
                String frameworkAttributeName = attrDef.getFrameworkAttributeName();
                if (Name.NAME.equals(frameworkAttributeName)) {
                    nameValue = ((ResourceAttribute<String>) attr).getValue().getValue();
                }
            }
        }
        if (uidValue != null) {
            if (nameValue == null) {
                return new Uid(uidValue);
            } else {
                return new Uid(uidValue, new Name(nameValue));
            }
        }
        // fallback, compatibility
        for (ResourceAttribute<?> attr : identifiers) {
            if (attr.getElementName().equals(SchemaConstants.ICFS_UID)) {
                return new Uid(((ResourceAttribute<String>) attr).getValue().getValue());
            }
        }
        return null;
    }

    private void replaceUidValue(ObjectClassComplexTypeDefinition objectClass,
            Collection<? extends ResourceAttribute<?>> identifiers, Uid newUid) {
        if (identifiers.size() == 0) {
            throw new IllegalStateException("No identifiers");
        }
        if (identifiers.size() == 1) {
            identifiers.iterator().next().setValue(new PrismPropertyValue(newUid.getUidValue()));
            return;
        }
        for (ResourceAttribute<?> attr : identifiers) {
            if (objectClass.isPrimaryIdentifier(attr.getElementName())) {
                ((ResourceAttribute<String>) attr).setValue(new PrismPropertyValue(newUid.getUidValue()));
                return;
            }
        }
        // fallback, compatibility
        for (ResourceAttribute<?> attr : identifiers) {
            if (attr.getElementName().equals(SchemaConstants.ICFS_UID)) {
                attr.setValue(new PrismPropertyValue(newUid.getUidValue())); // expecting the UID property is of type String
                return;
            }
        }
        throw new IllegalStateException("No UID attribute in " + identifiers);
    }

    private ResourceAttributeDefinition getUidDefinition(ObjectClassComplexTypeDefinition objectClass,
            Collection<? extends ResourceAttribute<?>> identifiers) {
        if (identifiers.size() == 0) {
            return null;
        }
        if (identifiers.size() == 1) {
            return identifiers.iterator().next().getDefinition();
        }
        for (ResourceAttribute<?> attr : identifiers) {
            if (objectClass.isPrimaryIdentifier(attr.getElementName())) {
                return ((ResourceAttribute<String>) attr).getDefinition();
            }
        }
        // fallback, compatibility
        for (ResourceAttribute<?> attr : identifiers) {
            if (attr.getElementName().equals(SchemaConstants.ICFS_UID)) {
                return attr.getDefinition();
            }
        }
        return null;
    }

    private void convertFromActivation(Set<Attribute> updateAttributes,
            Collection<PropertyDelta<?>> activationDeltas) throws SchemaException {

        for (PropertyDelta<?> propDelta : activationDeltas) {
            if (propDelta.getElementName().equals(ActivationType.F_ADMINISTRATIVE_STATUS)) {
                ActivationStatusType status = getPropertyNewValue(propDelta, ActivationStatusType.class);
                if (status == null) {
                    updateAttributes.add(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME));
                } else {
                    updateAttributes.add(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME,
                            status == ActivationStatusType.ENABLED));
                }
            } else if (propDelta.getElementName().equals(ActivationType.F_VALID_FROM)) {
                XMLGregorianCalendar xmlCal = getPropertyNewValue(propDelta, XMLGregorianCalendar.class);//propDelta.getPropertyNew().getValue(XMLGregorianCalendar.class).getValue();
                updateAttributes.add(AttributeBuilder.build(OperationalAttributes.ENABLE_DATE_NAME,
                        xmlCal != null ? XmlTypeConverter.toMillis(xmlCal) : null));
            } else if (propDelta.getElementName().equals(ActivationType.F_VALID_TO)) {
                XMLGregorianCalendar xmlCal = getPropertyNewValue(propDelta, XMLGregorianCalendar.class);//propDelta.getPropertyNew().getValue(XMLGregorianCalendar.class).getValue();
                updateAttributes.add(AttributeBuilder.build(OperationalAttributes.DISABLE_DATE_NAME,
                        xmlCal != null ? XmlTypeConverter.toMillis(xmlCal) : null));
            } else if (propDelta.getElementName().equals(ActivationType.F_LOCKOUT_STATUS)) {
                LockoutStatusType status = getPropertyNewValue(propDelta, LockoutStatusType.class);//propDelta.getPropertyNew().getValue(LockoutStatusType.class).getValue();
                updateAttributes.add(AttributeBuilder.build(OperationalAttributes.LOCK_OUT_NAME,
                        status != LockoutStatusType.NORMAL));
            } else {
                throw new SchemaException("Got unknown activation attribute delta " + propDelta.getElementName());
            }
        }

    }

    private <T> T getPropertyNewValue(PropertyDelta propertyDelta, Class<T> clazz) throws SchemaException {
        PrismProperty<PrismPropertyValue<T>> prop = propertyDelta.getPropertyNewMatchingPath();
        if (prop == null) {
            return null;
        }
        PrismPropertyValue<T> propValue = prop.getValue(clazz);

        if (propValue == null) {
            return null;
        }

        return propValue.getValue();
    }

    private void convertFromPassword(Set<Attribute> attributes, PropertyDelta<ProtectedStringType> passwordDelta)
            throws SchemaException {
        if (passwordDelta == null) {
            throw new IllegalArgumentException("No password was provided");
        }

        QName elementName = passwordDelta.getElementName();
        if (StringUtils.isBlank(elementName.getNamespaceURI())) {
            if (!QNameUtil.match(elementName, PasswordType.F_VALUE)) {
                return;
            }
        } else if (!passwordDelta.getElementName().equals(PasswordType.F_VALUE)) {
            return;
        }
        PrismProperty<ProtectedStringType> newPassword = passwordDelta.getPropertyNewMatchingPath();
        if (newPassword == null || newPassword.isEmpty()) {
            // This is the case of setting no password. E.g. removing existing password
            LOGGER.debug("Setting null password.");
            attributes.add(AttributeBuilder.build(OperationalAttributes.PASSWORD_NAME, Collections.EMPTY_LIST));
        } else if (newPassword.getRealValue().canGetCleartext()) {
            // We have password and we can get a cleartext value of the passowrd. This is normal case
            GuardedString guardedPassword = ConnIdUtil.toGuardedString(newPassword.getRealValue(), "new password",
                    protector);
            attributes.add(AttributeBuilder.build(OperationalAttributes.PASSWORD_NAME, guardedPassword));
        } else {
            // We have password, but we cannot get a cleartext value. Just to nothing.
            LOGGER.debug(
                    "We would like to set password, but we do not have cleartext value. Skipping the opearation.");
        }
    }

    private void addConvertedValues(Collection<PrismPropertyValue<QName>> pvals, Set<Attribute> attributes,
            Map<QName, ObjectClassComplexTypeDefinition> auxiliaryObjectClassMap) throws SchemaException {
        if (pvals == null) {
            return;
        }
        AttributeBuilder ab = new AttributeBuilder();
        ab.setName(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME);
        for (PrismPropertyValue<QName> pval : pvals) {
            QName auxQName = pval.getValue();
            ObjectClassComplexTypeDefinition auxDef = resourceSchema.findObjectClassDefinition(auxQName);
            if (auxDef == null) {
                throw new SchemaException("Auxiliary object class " + auxQName + " not found in the schema");
            }
            auxiliaryObjectClassMap.put(auxQName, auxDef);
            ObjectClass icfOc = connIdNameMapper.objectClassToIcf(pval.getValue(), resourceSchemaNamespace,
                    connectorType, false);
            ab.addValue(icfOc.getObjectClassValue());
        }
        attributes.add(ab.build());
    }

    private List<Change> getChangesFromSyncDeltas(ObjectClass connIdObjClass, Collection<SyncDelta> connIdDeltas,
            PrismSchema schema, OperationResult parentResult) throws SchemaException, GenericFrameworkException {
        List<Change> changeList = new ArrayList<>();

        QName objectClass = connIdNameMapper.objectClassToQname(connIdObjClass, getSchemaNamespace(), legacySchema);
        ObjectClassComplexTypeDefinition objClassDefinition = null;
        if (objectClass != null) {
            objClassDefinition = (ObjectClassComplexTypeDefinition) schema.findComplexTypeDefinition(objectClass);
        }

        Validate.notNull(connIdDeltas, "Sync result must not be null.");
        for (SyncDelta icfDelta : connIdDeltas) {

            ObjectClass deltaIcfObjClass = connIdObjClass;
            QName deltaObjectClass = objectClass;
            ObjectClassComplexTypeDefinition deltaObjClassDefinition = objClassDefinition;
            if (objectClass == null) {
                deltaIcfObjClass = icfDelta.getObjectClass();
                deltaObjectClass = connIdNameMapper.objectClassToQname(deltaIcfObjClass, getSchemaNamespace(),
                        legacySchema);
                if (deltaIcfObjClass != null) {
                    deltaObjClassDefinition = (ObjectClassComplexTypeDefinition) schema
                            .findComplexTypeDefinition(deltaObjectClass);
                }
            }
            if (deltaObjClassDefinition == null) {
                if (icfDelta.getDeltaType() == SyncDeltaType.DELETE) {
                    // tolerate this. E.g. LDAP changelogs do not have objectclass in delete deltas.
                } else {
                    throw new SchemaException("Got delta with object class " + deltaObjectClass + " ("
                            + deltaIcfObjClass + ") that has no definition in resource schema");
                }
            }

            SyncDeltaType icfDeltaType = icfDelta.getDeltaType();
            if (SyncDeltaType.DELETE.equals(icfDeltaType)) {
                LOGGER.trace("START creating delta of type DELETE");
                ObjectDelta<ShadowType> objectDelta = new ObjectDelta<>(ShadowType.class, ChangeType.DELETE,
                        prismContext);
                Collection<ResourceAttribute<?>> identifiers = ConnIdUtil.convertToIdentifiers(icfDelta.getUid(),
                        deltaObjClassDefinition, resourceSchema);
                Change change = new Change(identifiers, objectDelta, getToken(icfDelta.getToken()));
                change.setObjectClassDefinition(deltaObjClassDefinition);
                changeList.add(change);
                LOGGER.trace("END creating delta of type DELETE");

            } else if (SyncDeltaType.CREATE.equals(icfDeltaType)) {
                PrismObjectDefinition<ShadowType> objectDefinition = toShadowDefinition(deltaObjClassDefinition);
                LOGGER.trace("Object definition: {}", objectDefinition);

                LOGGER.trace("START creating delta of type CREATE");
                PrismObject<ShadowType> currentShadow = connIdConvertor.convertToResourceObject(
                        icfDelta.getObject(), objectDefinition, false, caseIgnoreAttributeNames, legacySchema);

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Got current shadow: {}", currentShadow.debugDump());
                }

                Collection<ResourceAttribute<?>> identifiers = ShadowUtil.getAllIdentifiers(currentShadow);

                ObjectDelta<ShadowType> objectDelta = new ObjectDelta<>(ShadowType.class, ChangeType.ADD,
                        prismContext);
                objectDelta.setObjectToAdd(currentShadow);

                Change change = new Change(identifiers, objectDelta, getToken(icfDelta.getToken()));
                change.setObjectClassDefinition(deltaObjClassDefinition);
                changeList.add(change);
                LOGGER.trace("END creating delta of type CREATE");

            } else if (SyncDeltaType.CREATE_OR_UPDATE.equals(icfDeltaType)
                    || SyncDeltaType.UPDATE.equals(icfDeltaType)) {
                PrismObjectDefinition<ShadowType> objectDefinition = toShadowDefinition(deltaObjClassDefinition);
                LOGGER.trace("Object definition: {}", objectDefinition);

                LOGGER.trace("START creating delta of type {}", icfDeltaType);
                PrismObject<ShadowType> currentShadow = connIdConvertor.convertToResourceObject(
                        icfDelta.getObject(), objectDefinition, false, caseIgnoreAttributeNames, legacySchema);

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Got current shadow: {}", currentShadow.debugDump());
                }

                Collection<ResourceAttribute<?>> identifiers = ShadowUtil.getAllIdentifiers(currentShadow);

                Change change = new Change(identifiers, currentShadow, getToken(icfDelta.getToken()));
                change.setObjectClassDefinition(deltaObjClassDefinition);
                changeList.add(change);
                LOGGER.trace("END creating delta of type {}:\n{}", icfDeltaType, change.debugDump());

            } else {
                throw new GenericFrameworkException("Unexpected sync delta type " + icfDeltaType);
            }

        }
        return changeList;
    }

    private SyncToken getSyncToken(PrismProperty tokenProperty) throws SchemaException {
        if (tokenProperty == null) {
            return null;
        }
        if (tokenProperty.getValues() == null) {
            return null;
        }

        if (tokenProperty.getValues().isEmpty()) {
            return null;
        }

        if (tokenProperty.getValues().size() > 1) {
            throw new SchemaException(
                    "Unexpected number of attributes in SyncToken. SyncToken is single-value attribute and can contain only one value.");
        }

        Object tokenValue = tokenProperty.getAnyRealValue();
        if (tokenValue == null) {
            return null;
        }

        SyncToken syncToken = new SyncToken(tokenValue);
        return syncToken;
    }

    private <T> PrismProperty<T> getToken(SyncToken syncToken) {
        T object = (T) syncToken.getValue();
        return createTokenProperty(object);
    }

    private <T> PrismProperty<T> createTokenProperty(T object) {
        QName type = XsdTypeMapper.toXsdType(object.getClass());

        Set<PrismPropertyValue<T>> syncTokenValues = new HashSet<>();
        syncTokenValues.add(new PrismPropertyValue<>(object));
        PrismPropertyDefinitionImpl propDef = new PrismPropertyDefinitionImpl(SchemaConstants.SYNC_TOKEN, type,
                prismContext);
        propDef.setDynamic(true);
        propDef.setMaxOccurs(1);
        PrismProperty<T> property = propDef.instantiate();
        property.addValues(syncTokenValues);
        return property;
    }

    /**
     * check additional operation order, according to the order are script
     * executed before or after operation..
     */
    private void checkAndExecuteAdditionalOperations(StateReporter reporter,
            Collection<Operation> additionalOperations, BeforeAfterType order, OperationResult result)
            throws CommunicationException, GenericFrameworkException {

        if (additionalOperations == null) {
            // TODO: add warning to the result
            return;
        }

        for (Operation op : additionalOperations) {
            if (op instanceof ExecuteProvisioningScriptOperation) {
                ExecuteProvisioningScriptOperation executeOp = (ExecuteProvisioningScriptOperation) op;
                LOGGER.trace("Find execute script operation: {}", SchemaDebugUtil.prettyPrint(executeOp));
                // execute operation in the right order..
                if (order.equals(executeOp.getScriptOrder())) {
                    executeScriptIcf(reporter, executeOp, result);
                }
            }
        }

    }

    @Override
    public Object executeScript(ExecuteProvisioningScriptOperation scriptOperation, StateReporter reporter,
            OperationResult parentResult) throws CommunicationException, GenericFrameworkException {

        OperationResult result = parentResult.createSubresult(ConnectorInstance.class.getName() + ".executeScript");

        Object output = null;
        try {

            output = executeScriptIcf(reporter, scriptOperation, result);

        } catch (CommunicationException e) {
            result.recordFatalError(e);
            throw e;
        } catch (GenericFrameworkException e) {
            result.recordFatalError(e);
            throw e;
        } catch (RuntimeException e) {
            result.recordFatalError(e);
            throw e;
        }

        result.computeStatus();

        return output;
    }

    private Object executeScriptIcf(StateReporter reporter, ExecuteProvisioningScriptOperation scriptOperation,
            OperationResult parentResult) throws CommunicationException, GenericFrameworkException {

        String icfOpName = null;
        if (scriptOperation.isConnectorHost()) {
            icfOpName = "runScriptOnConnector";
        } else if (scriptOperation.isResourceHost()) {
            icfOpName = "runScriptOnResource";
        } else {
            parentResult.recordFatalError("Where to execute the script?");
            throw new IllegalArgumentException("Where to execute the script?");
        }

        // convert execute script operation to the script context required from
        // the connector
        ScriptContext scriptContext = convertToScriptContext(scriptOperation);

        OperationResult icfResult = parentResult.createSubresult(ConnectorFacade.class.getName() + "." + icfOpName);
        icfResult.addContext("connector", connIdConnectorFacade.getClass());

        Object output = null;

        try {

            LOGGER.trace("Running script ({})", icfOpName);

            recordIcfOperationStart(reporter, ProvisioningOperation.ICF_SCRIPT, null);
            if (scriptOperation.isConnectorHost()) {
                InternalMonitor.recordConnectorOperation("runScriptOnConnector");
                output = connIdConnectorFacade.runScriptOnConnector(scriptContext,
                        new OperationOptionsBuilder().build());
            } else if (scriptOperation.isResourceHost()) {
                InternalMonitor.recordConnectorOperation("runScriptOnResource");
                output = connIdConnectorFacade.runScriptOnResource(scriptContext,
                        new OperationOptionsBuilder().build());
            }
            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SCRIPT, null);

            icfResult.recordSuccess();

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Finished running script ({}), script result: {}", icfOpName,
                        PrettyPrinter.prettyPrint(output));
            }

        } catch (Throwable ex) {

            recordIcfOperationEnd(reporter, ProvisioningOperation.ICF_SCRIPT, null, ex);

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Finished running script ({}), ERROR: {}", icfOpName, ex.getMessage());
            }

            Throwable midpointEx = processConnIdException(ex, this, icfResult);

            CriticalityType criticality = scriptOperation.getCriticality();
            if (criticality == null || criticality == CriticalityType.FATAL) {
                parentResult.computeStatus();
                // Do some kind of acrobatics to do proper throwing of checked
                // exception
                if (midpointEx instanceof CommunicationException) {
                    throw (CommunicationException) midpointEx;
                } else if (midpointEx instanceof GenericFrameworkException) {
                    throw (GenericFrameworkException) midpointEx;
                } else if (midpointEx instanceof SchemaException) {
                    // Schema exception during delete? It must be a missing UID
                    throw new IllegalArgumentException(midpointEx.getMessage(), midpointEx);
                } else if (midpointEx instanceof RuntimeException) {
                    throw (RuntimeException) midpointEx;
                } else if (midpointEx instanceof Error) {
                    throw (Error) midpointEx;
                } else {
                    throw new SystemException(
                            "Got unexpected exception: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
                }

            } else if (criticality == CriticalityType.PARTIAL) {
                icfResult.setStatus(OperationResultStatus.PARTIAL_ERROR);
                parentResult.computeStatus();
            }
        }

        return output;
    }

    private ScriptContext convertToScriptContext(ExecuteProvisioningScriptOperation executeOp) {
        // creating script arguments map form the execute script operation
        // arguments
        Map<String, Object> scriptArguments = new HashMap<>();
        for (ExecuteScriptArgument argument : executeOp.getArgument()) {
            scriptArguments.put(argument.getArgumentName(), argument.getArgumentValue());
        }
        ScriptContext scriptContext = new ScriptContext(executeOp.getLanguage(), executeOp.getTextCode(),
                scriptArguments);
        return scriptContext;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "ConnectorInstanceIcfImpl(" + connectorType + ")";
    }

    public String getHumanReadableName() {
        return connectorType.toString() + ": " + description;
    }

    @Override
    public void dispose() {
        // Nothing to do
    }

    private void recordIcfOperationStart(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition, Uid uid) {
        if (reporter != null) {
            reporter.recordIcfOperationStart(operation, objectClassDefinition,
                    uid == null ? null : uid.getUidValue());
        } else {
            LOGGER.warn("Couldn't record ConnId operation start as reporter is null.");
        }
    }

    private void recordIcfOperationStart(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition) {
        if (reporter != null) {
            reporter.recordIcfOperationStart(operation, objectClassDefinition, null);
        } else {
            LOGGER.warn("Couldn't record ConnId operation start as reporter is null.");
        }
    }

    private void recordIcfOperationResume(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition) {
        if (reporter != null) {
            reporter.recordIcfOperationResume(operation, objectClassDefinition);
        } else {
            LOGGER.warn("Couldn't record ConnId operation resume as reporter is null.");
        }
    }

    private void recordIcfOperationSuspend(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition) {
        if (reporter != null) {
            reporter.recordIcfOperationSuspend(operation, objectClassDefinition);
        } else {
            LOGGER.warn("Couldn't record ConnId operation suspension as reporter is null.");
        }
    }

    private void recordIcfOperationEnd(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition, Uid uid) {
        if (reporter != null) {
            reporter.recordIcfOperationEnd(operation, objectClassDefinition, null,
                    uid == null ? null : uid.getUidValue());
        } else {
            LOGGER.warn("Couldn't record ConnId operation end as reporter is null.");
        }
    }

    private void recordIcfOperationEnd(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition, Throwable ex) {
        if (reporter != null) {
            reporter.recordIcfOperationEnd(operation, objectClassDefinition, ex, null);
        } else {
            LOGGER.warn("Couldn't record ConnId operation end as reporter is null.");
        }
    }

    private void recordIcfOperationEnd(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition, Throwable ex, Uid uid) {
        if (reporter != null) {
            reporter.recordIcfOperationEnd(operation, objectClassDefinition, ex,
                    uid == null ? null : uid.getUidValue());
        } else {
            LOGGER.warn("Couldn't record ConnId operation end as reporter is null.");
        }
    }

    private void recordIcfOperationEnd(StateReporter reporter, ProvisioningOperation operation,
            ObjectClassComplexTypeDefinition objectClassDefinition) {
        if (reporter != null) {
            reporter.recordIcfOperationEnd(operation, objectClassDefinition, null, null);
        } else {
            LOGGER.warn("Couldn't record ConnId operation end as reporter is null.");
        }
    }

}