org.forgerock.openidm.provisioner.openicf.impl.OpenICFProvisionerService.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.provisioner.openicf.impl.OpenICFProvisionerService.java

Source

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2011-2015 ForgeRock AS.
 */
package org.forgerock.openidm.provisioner.openicf.impl;

import static org.forgerock.json.JsonValue.array;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newQueryResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.util.promise.Promises.newResultPromise;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.and;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.contains;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.containsAllValues;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.endsWith;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.equalTo;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.greaterThan;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.greaterThanOrEqualTo;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.lessThan;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.lessThanOrEqualTo;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.not;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.or;
import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.startsWith;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.forgerock.guava.common.base.Predicate;
import org.forgerock.guava.common.collect.FluentIterable;
import org.forgerock.services.context.Context;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.json.crypto.JsonCryptoException;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.ConflictException;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.ForbiddenException;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryFilters;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ServiceUnavailableException;
import org.forgerock.json.resource.SingletonResourceProvider;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.json.resource.http.HttpContext;
import org.forgerock.openidm.audit.util.ActivityLogger;
import org.forgerock.openidm.audit.util.NullActivityLogger;
import org.forgerock.openidm.audit.util.RouterActivityLogger;
import org.forgerock.openidm.audit.util.Status;
import org.forgerock.openidm.config.enhanced.EnhancedConfig;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.openidm.provisioner.ProvisionerService;
import org.forgerock.openidm.provisioner.SimpleSystemIdentifier;
import org.forgerock.openidm.provisioner.SystemIdentifier;
import org.forgerock.openidm.provisioner.openicf.ConnectorInfoProvider;
import org.forgerock.openidm.provisioner.openicf.ConnectorReference;
import org.forgerock.openidm.provisioner.openicf.OperationHelper;
import org.forgerock.openidm.provisioner.openicf.commons.AttributeMissingException;
import org.forgerock.openidm.provisioner.openicf.commons.ConnectorUtil;
import org.forgerock.openidm.provisioner.openicf.commons.ObjectClassInfoHelper;
import org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper;
import org.forgerock.openidm.provisioner.openicf.internal.SystemAction;
import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncFailureHandler;
import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncFailureHandlerFactory;
import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncHandlerException;
import org.forgerock.openidm.router.IDMConnectionFactory;
import org.forgerock.openidm.router.RouteBuilder;
import org.forgerock.openidm.router.RouteEntry;
import org.forgerock.openidm.router.RouterRegistry;
import org.forgerock.openidm.smartevent.EventEntry;
import org.forgerock.openidm.smartevent.Publisher;
import org.forgerock.openidm.util.ContextUtil;
import org.forgerock.openidm.util.ResourceUtil;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.query.QueryFilter;
import org.forgerock.util.query.QueryFilterVisitor;
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.AuthenticationApiOp;
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.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.FrameworkUtil;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectionBrokenException;
import org.identityconnectors.framework.common.exceptions.ConnectionFailedException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.exceptions.ConnectorIOException;
import org.identityconnectors.framework.common.exceptions.ConnectorSecurityException;
import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException;
import org.identityconnectors.framework.common.exceptions.InvalidCredentialException;
import org.identityconnectors.framework.common.exceptions.InvalidPasswordException;
import org.identityconnectors.framework.common.exceptions.OperationTimeoutException;
import org.identityconnectors.framework.common.exceptions.PasswordExpiredException;
import org.identityconnectors.framework.common.exceptions.PermissionDeniedException;
import org.identityconnectors.framework.common.exceptions.PreconditionFailedException;
import org.identityconnectors.framework.common.exceptions.PreconditionRequiredException;
import org.identityconnectors.framework.common.exceptions.RetryableException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeUtil;
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.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationOptionsBuilder;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.ScriptContextBuilder;
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.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.common.serializer.SerializerUtil;
import org.identityconnectors.framework.impl.api.local.LocalConnectorFacadeImpl;
import org.identityconnectors.framework.impl.api.remote.RemoteWrappedException;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.ComponentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The OpenICFProvisionerService is the implementation of
 * {@link CollectionResourceProvider} interface with <a
 * href="http://openicf.forgerock.org">OpenICF</a>.
 * <p/>
 */
@Component(name = OpenICFProvisionerService.PID, policy = ConfigurationPolicy.REQUIRE, metatype = true, description = "OpenIDM OpenICF Provisioner Service", immediate = true)
@Service(value = { ProvisionerService.class })
@Properties({ @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME),
        @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM OpenICF Provisioner Service") })
public class OpenICFProvisionerService implements ProvisionerService, SingletonResourceProvider {

    // Public Constants
    public static final String PID = "org.forgerock.openidm.provisioner.openicf";

    //Private Constants
    private static final String REAUTH_HEADER = "X-OpenIDM-Reauth-Password";
    private static final String RUN_AS_USER = "runAsUser";
    private static final String ACCOUNT_USERNAME_ATTRIBUTES = "accountUserNameAttributes";

    private static final Logger logger = LoggerFactory.getLogger(OpenICFProvisionerService.class);

    // Monitoring event name prefix
    private static final String EVENT_PREFIX = "openidm/internal/system/";

    private static final int UNAUTHORIZED_ERROR_CODE = 401;

    private SimpleSystemIdentifier systemIdentifier = null;
    private OperationHelperBuilder operationHelperBuilder = null;
    private Promise<ConnectorInfo, RuntimeException> connectorFacadeCallback = null;
    private boolean serviceAvailable = false;
    private JsonValue jsonConfiguration = null;
    private ConnectorReference connectorReference = null;
    private SyncFailureHandler syncFailureHandler = null;
    private String factoryPid = null;

    /** use null-object activity logger until/unless ConnectionFactory binder updates it */
    private ActivityLogger activityLogger = NullActivityLogger.INSTANCE;

    /**
     * Cache the SystemActions from local and {@code provisioner.json} jsonConfiguration.
     */
    private final ConcurrentMap<String, SystemAction> localSystemActionCache = new ConcurrentHashMap<String, SystemAction>();

    private final ConcurrentMap<String, RequestHandler> objectClassHandlers = new ConcurrentHashMap<String, RequestHandler>();

    /* Internal routing objects to register and remove the routes. */
    private RouteEntry routeEntry;

    /* Object Types*/
    private Map<String, ObjectClassInfoHelper> objectTypes;

    /**
     * Holder of non ObjectClass operations:
     *
     * <pre>
     * ValidateApiOp
     * TestApiOp
     * ScriptOnConnectorApiOp
     * ScriptOnResourceApiOp
     * SchemaApiOp
     * </pre>
     */
    private Map<Class<? extends APIOperation>, OperationOptionInfoHelper> systemOperations = null;

    /** The Connection Factory */
    @Reference(policy = ReferencePolicy.STATIC)
    protected IDMConnectionFactory connectionFactory;

    void bindConnectionFactory(final IDMConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
        // update activityLogger to use the "real" activity logger on the router
        this.activityLogger = new RouterActivityLogger(connectionFactory);
    }

    void unbindConnectionFactory(final IDMConnectionFactory connectionFactory) {
        this.connectionFactory = null;
        // ConnectionFactory has gone away, use null activity logger
        this.activityLogger = NullActivityLogger.INSTANCE;
    }

    /**
     * ConnectorInfoProvider service.
     */
    @Reference(policy = ReferencePolicy.DYNAMIC)
    protected ConnectorInfoProvider connectorInfoProvider = null;

    /**
     * RouterRegistryService service.
     */
    @Reference(policy = ReferencePolicy.STATIC)
    protected RouterRegistry routerRegistry;

    /**
     * Cryptographic service.
     */
    @Reference(policy = ReferencePolicy.DYNAMIC)
    protected CryptoService cryptoService = null;

    /**
     * SyncFailureHandlerFactory service.
     */
    @Reference
    protected SyncFailureHandlerFactory syncFailureHandlerFactory = null;

    /**
     * Enhanced configuration service.
     */
    @Reference(policy = ReferencePolicy.DYNAMIC)
    private EnhancedConfig enhancedConfig;

    /**
     * Reference to the ThreadSafe {@code ConnectorFacade} instance.
     */
    private final AtomicReference<ConnectorFacade> connectorFacade = new AtomicReference<ConnectorFacade>();

    @Activate
    protected void activate(ComponentContext context) {
        try {
            factoryPid = (String) context.getProperties().get("config.factory-pid");
            jsonConfiguration = enhancedConfig.getConfigurationAsJson(context);
            systemIdentifier = new SimpleSystemIdentifier(jsonConfiguration);

            if (!jsonConfiguration.get("enabled").defaultTo(true).asBoolean()) {
                logger.info("OpenICF Provisioner Service {} is disabled, \"enabled\" set to false in configuration",
                        systemIdentifier.getName());
                return;
            }

            loadLocalSystemActions(jsonConfiguration);

            connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration);

            syncFailureHandler = syncFailureHandlerFactory.create(jsonConfiguration.get("syncFailureHandler"));

            connectorInfoProvider.findConnectorInfoAsync(connectorReference)
                    .thenOnResult(new org.forgerock.util.promise.ResultHandler<ConnectorInfo>() {
                        public void handleResult(ConnectorInfo connectorInfo) {
                            try {
                                APIConfiguration config = connectorInfo.createDefaultAPIConfiguration();
                                operationHelperBuilder = new OperationHelperBuilder(systemIdentifier.getName(),
                                        jsonConfiguration, config, cryptoService);
                                try {
                                    Map<String, Map<Class<? extends APIOperation>, OperationOptionInfoHelper>> objectOperations = ConnectorUtil
                                            .getOperationOptionConfiguration(jsonConfiguration);

                                    objectTypes = ConnectorUtil.getObjectTypes(jsonConfiguration);

                                    for (Map.Entry<String, ObjectClassInfoHelper> entry : objectTypes.entrySet()) {

                                        objectClassHandlers.put(entry.getKey(),
                                                new ObjectClassResourceProvider(entry.getKey(), entry.getValue(),
                                                        objectOperations.get(entry.getKey())));
                                    }

                                    // TODO Fix this Map
                                    // ValidateApiOp
                                    // TestApiOp
                                    // ScriptOnConnectorApiOp
                                    // ScriptOnResourceApiOp
                                    // SchemaApiOp
                                    systemOperations = Collections.emptyMap();
                                } catch (Exception e) {
                                    logger.error("OpenICF connector jsonConfiguration of {} has errors.",
                                            systemIdentifier.getName(), e);
                                    throw new ComponentException(
                                            "OpenICF connector jsonConfiguration has errors and the service can not be initiated.",
                                            e);
                                }

                                ConnectorUtil.configureDefaultAPIConfiguration(jsonConfiguration, config,
                                        cryptoService);

                                final ConnectorFacade facade = connectorInfoProvider.createConnectorFacade(config);

                                if (null == facade) {
                                    logger.warn("OpenICF ConnectorFacade of {} is not available",
                                            connectorReference);
                                } else {
                                    facade.validate();
                                    if (connectorFacade.compareAndSet(null, facade)) {
                                        if (facade.getSupportedOperations().contains(TestApiOp.class)) {
                                            try {
                                                facade.test();
                                                logger.debug("OpenICF connector test of {} succeeded!",
                                                        systemIdentifier);
                                                serviceAvailable = true;
                                            } catch (InvalidCredentialException e) {
                                                logger.error("Connection error for {} ", systemIdentifier, e);
                                            } catch (Exception e) {
                                                logger.error("OpenICF connector test of {} failed!",
                                                        systemIdentifier, e);
                                            }
                                        } else {
                                            logger.debug("OpenICF connector of {} does not support test.",
                                                    connectorReference);
                                            serviceAvailable = true;
                                        }
                                    }
                                }
                                logger.info("OpenICF Provisioner Service component {} is activated.",
                                        systemIdentifier.getName());
                            } catch (Throwable t) {
                                logger.warn(t.getMessage());
                            }
                        }
                    });

            routeEntry = routerRegistry.addRoute(RouteBuilder.newBuilder()
                    .withTemplate(ProvisionerService.ROUTER_PREFIX + "/" + systemIdentifier.getName())
                    .withSingletonResourceProvider(this).buildNext().withModeStartsWith()
                    .withTemplate(ProvisionerService.ROUTER_PREFIX + "/" + systemIdentifier.getName()
                            + ObjectClassRequestHandler.OBJECTCLASS_TEMPLATE)
                    .withRequestHandler(new ObjectClassRequestHandler()).seal());

            logger.info("OpenICF Provisioner Service component {} route enabled{}", systemIdentifier.getName(),
                    (null != connectorFacade.get() ? "." : " although the service is not available yet."));
        } catch (Exception e) {
            logger.error("OpenICF Provisioner Service configuration has errors", e);
            throw new ComponentException("OpenICF Provisioner Service configuration has errors", e);
        }
    }

    @Deactivate
    protected void deactivate(ComponentContext context) {
        if (null != connectorFacadeCallback) {
            connectorFacadeCallback.cancel(false);
            connectorFacadeCallback = null;
        }
        if (null != routeEntry) {
            routeEntry.removeRoute();
            routeEntry = null;
        }
        if (connectorFacade.get() instanceof LocalConnectorFacadeImpl) {
            ((LocalConnectorFacadeImpl) connectorFacade.get()).dispose();
        }
        connectorFacade.set(null);
        logger.info("OpenICF Provisioner Service component {} is deactivated.", systemIdentifier.getName());
        systemIdentifier = null;
    }

    private void loadLocalSystemActions(JsonValue configuration) {
        // TODO delay initialization /config/system

        if (configuration.isDefined("systemActions")) {
            for (JsonValue actionValue : configuration.get("systemActions").expect(List.class)) {
                SystemAction action = new SystemAction(actionValue);
                localSystemActionCache.put(action.getName(), action);
            }
        }
    }

    ConnectorFacade getConnectorFacade() {
        return connectorFacade.get();
    }

    /**
     * Handle ConnectorExceptions from ConnectorFacade invocations.  Maps each ConnectorException subtype to the
     * appropriate {@link ResourceException} for passing to {@code handleError}.  Optionally logs to activity log.
     *
     * @param context the Context from the original request
     * @param request the original request
     * @param exception the ConnectorException that was thrown by the facade
     * @param resourceId the resourceId being operated on
     * @param before the object value "before" the request
     * @param after the object value "after" the request
     * @param connectorExceptionActivityLogger the ActivityLogger to use to log the exception
     */
    private ResourceException adaptConnectorException(Context context, Request request,
            ConnectorException exception, String resourceContainer, String resourceId, JsonValue before,
            JsonValue after, ActivityLogger connectorExceptionActivityLogger) {

        // default message
        String message = MessageFormat.format("Operation {0} failed with {1} on system object: {2}",
                request.getRequestType(), exception.getClass().getSimpleName(), resourceId);

        try {
            throw exception;
        } catch (AlreadyExistsException e) {
            message = MessageFormat.format("System object {0} already exists", resourceId);
            // TODO-crest3
            return new org.forgerock.json.resource.PreconditionFailedException(message, exception);
        } catch (ConfigurationException e) {
            message = MessageFormat.format("Operation {0} failed with ConfigurationException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new InternalServerErrorException(message, exception);
        } catch (ConnectionBrokenException e) {
            message = MessageFormat.format(
                    "Operation {0} failed with ConnectionBrokenException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ServiceUnavailableException(message, exception);
        } catch (ConnectionFailedException e) {
            message = MessageFormat.format("Connection failed during operation {0} on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ServiceUnavailableException(message, exception);
        } catch (ConnectorIOException e) {
            message = MessageFormat.format("Operation {0} failed with ConnectorIOException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ServiceUnavailableException(message, exception);
        } catch (OperationTimeoutException e) {
            message = MessageFormat.format("Operation {0} Timeout on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ServiceUnavailableException(message, exception);
        } catch (PasswordExpiredException e) {
            message = MessageFormat.format(
                    "Operation {0} failed with PasswordExpiredException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ForbiddenException(message, exception);
        } catch (InvalidPasswordException e) {
            message = MessageFormat.format(
                    "Invalid password has been provided to operation {0} for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return ResourceException.getException(UNAUTHORIZED_ERROR_CODE, message, exception);
        } catch (UnknownUidException e) {
            message = MessageFormat.format("Operation {0} could not find resource {1} on system object: {2}",
                    request.getRequestType().toString(), resourceId, resourceContainer);
            return new NotFoundException(message, exception)
                    .setDetail(new JsonValue(new HashMap<String, Object>()));
        } catch (InvalidCredentialException e) {
            message = MessageFormat.format(
                    "Invalid credential has been provided to operation {0} for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return ResourceException.getException(UNAUTHORIZED_ERROR_CODE, message, exception);
        } catch (PermissionDeniedException e) {
            message = MessageFormat.format("Permission was denied on {0} operation for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ForbiddenException(message, exception);
        } catch (ConnectorSecurityException e) {
            message = MessageFormat.format(
                    "Operation {0} failed with ConnectorSecurityException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new InternalServerErrorException(message, exception);
        } catch (InvalidAttributeValueException e) {
            message = MessageFormat.format(
                    "Attribute value conflicts with the attribute''s schema definition on "
                            + "operation {0} for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new BadRequestException(message, exception);
        } catch (PreconditionFailedException e) {
            message = MessageFormat.format(
                    "The resource version for {0} does not match the version provided on "
                            + "operation {1} for system object: {2}",
                    resourceId, request.getRequestType().toString(), resourceContainer);
            return new org.forgerock.json.resource.PreconditionFailedException(message, exception);
        } catch (PreconditionRequiredException e) {
            message = MessageFormat.format(
                    "No resource version for resource {0} has been provided on operation {1} for system object: {2}",
                    resourceId, request.getRequestType().toString(), resourceContainer);
            return new org.forgerock.json.resource.PreconditionRequiredException(message, exception);
        } catch (RetryableException e) {
            message = MessageFormat.format(
                    "Request temporarily unavailable on operation {0} for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new ServiceUnavailableException(message, exception);
        } catch (UnsupportedOperationException e) {
            message = MessageFormat.format("Operation {0} is no supported for system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new NotFoundException(message, exception);
        } catch (IllegalArgumentException e) {
            message = MessageFormat.format(
                    "Operation {0} failed with IllegalArgumentException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new InternalServerErrorException(message, e);
        } catch (RemoteWrappedException e) {
            return adaptRemoteWrappedException(context, request, exception, resourceContainer, resourceId, before,
                    after, connectorExceptionActivityLogger);
        } catch (ConnectorException e) {
            message = MessageFormat.format("Operation {0} failed with ConnectorException on system object: {1}",
                    request.getRequestType().toString(), resourceId);
            return new InternalServerErrorException(message, exception);
        } finally {
            // log the ConnectorException
            logger.debug(message, exception);
            try {
                connectorExceptionActivityLogger.log(context, request, message, resourceId, before, after,
                        Status.FAILURE);
            } catch (ResourceException e) {
                // this means the ActivityLogger couldn't log request; log to error log
                logger.warn("Failed to write activity log", e);
            }
        }
    }

    /**
     * .NET Exceptions that may be wrapped in a RemoteWrappedException
     */
    private enum DotNetExceptionHelper {

        ArgumentException("System.ArgumentException") {
            Exception getMappedException(Exception e) {
                return new IllegalArgumentException(e.getMessage(), e.getCause());
            }
        },
        InvalidOperationException("System.InvalidOperationException") {
            Exception getMappedException(Exception e) {
                return new IllegalStateException(e.getMessage(), e.getCause());
            }
        },
        NullReferenceException("System.NullReferenceException") {
            Exception getMappedException(Exception e) {
                return new NullPointerException(e.getMessage());
            }
        },
        NotSupportedException("System.NotSupportedException") {
            Exception getMappedException(Exception e) {
                return new UnsupportedOperationException(e.getMessage(), e.getCause());
            }
        },
        UnknownDotNetException("") {
            Exception getMappedException(Exception e) {
                return new InternalServerErrorException(e.getMessage(), e.getCause());
            }
        };

        private final String exceptionName;

        private DotNetExceptionHelper(final String exceptionName) {
            this.exceptionName = exceptionName;
        }

        abstract Exception getMappedException(Exception e);

        ConnectorException getConnectorException(Exception e) {
            return new ConnectorException(e.getMessage(), getMappedException(e));
        }

        static DotNetExceptionHelper fromExceptionClass(String name) {
            for (DotNetExceptionHelper helper : values()) {
                if (helper.exceptionName.equals(name)) {
                    return helper;
                }

            }
            return UnknownDotNetException;
        }
    }

    /**
     * Checks the RemoteWrappedException to determine which Exception has been wrapped and returns
     * the appropriated corresponding exception.
     *
     * @param context the Context from the original request
     * @param request the original request
     * @param exception the ConnectorException that was thrown by the facade
     * @param resourceId the resourceId being operated on
     * @param before the object value "before" the request
     * @param after the object value "after" the request
     * @param connectorExceptionActivityLogger the ActivityLogger to use to log the exception
     */
    private ResourceException adaptRemoteWrappedException(Context context, Request request,
            ConnectorException exception, String resourceContainer, String resourceId, JsonValue before,
            JsonValue after, ActivityLogger connectorExceptionActivityLogger) {

        RemoteWrappedException remoteWrappedException = (RemoteWrappedException) exception;
        final String message = exception.getMessage();
        final Throwable cause = exception.getCause();

        if (remoteWrappedException.is(AlreadyExistsException.class)) {
            return adaptConnectorException(context, request, new AlreadyExistsException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(ConfigurationException.class)) {
            return adaptConnectorException(context, request, new ConfigurationException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(ConnectionBrokenException.class)) {
            return adaptConnectorException(context, request, new ConnectionBrokenException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(ConnectionFailedException.class)) {
            return adaptConnectorException(context, request, new ConnectionFailedException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(ConnectorIOException.class)) {
            return adaptConnectorException(context, request, new ConnectorIOException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(InvalidAttributeValueException.class)) {
            return adaptConnectorException(context, request, new InvalidAttributeValueException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(InvalidCredentialException.class)) {
            return adaptConnectorException(context, request, new InvalidCredentialException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(InvalidPasswordException.class)) {
            return adaptConnectorException(context, request, new InvalidPasswordException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(OperationTimeoutException.class)) {
            return adaptConnectorException(context, request, new OperationTimeoutException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(PasswordExpiredException.class)) {
            return adaptConnectorException(context, request, new PasswordExpiredException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(PermissionDeniedException.class)) {
            return adaptConnectorException(context, request, new PermissionDeniedException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(PreconditionFailedException.class)) {
            return adaptConnectorException(context, request, new PreconditionFailedException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(PreconditionRequiredException.class)) {
            return adaptConnectorException(context, request, new PreconditionRequiredException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(RetryableException.class)) {
            return adaptConnectorException(context, request, RetryableException.wrap(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(UnknownUidException.class)) {
            return adaptConnectorException(context, request, new UnknownUidException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else if (remoteWrappedException.is(ConnectorException.class)) {
            return adaptConnectorException(context, request, new ConnectorException(message, cause),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        } else {
            // handle .NET exceptions
            return adaptConnectorException(context, request,
                    DotNetExceptionHelper.fromExceptionClass(remoteWrappedException.getExceptionClass())
                            .getConnectorException(remoteWrappedException),
                    resourceContainer, resourceId, before, after, connectorExceptionActivityLogger);
        }
    }

    /**
     * @return the smartevent Name for a given query
     */
    org.forgerock.openidm.smartevent.Name getQueryEventName(String objectClass, QueryRequest request) {
        String prefix = EVENT_PREFIX + getSystemIdentifierName() + "/" + objectClass + "/query/";

        if (request.getQueryId() != null) {
            return org.forgerock.openidm.smartevent.Name.get(prefix + request.getQueryId());
        } else if (request.getQueryExpression() != null) {
            return org.forgerock.openidm.smartevent.Name.get(prefix + "_query_expression");
        } else if (request.getQueryFilter() != null) {
            return org.forgerock.openidm.smartevent.Name.get(prefix + "_queryFilter");
        } else {
            // This should never happen...
            return org.forgerock.openidm.smartevent.Name.get(prefix + "_UNKNOWN");
        }
    }

    private enum ConnectorAction {
        script, test, livesync
    }

    @Override
    public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) {
        return new NotSupportedException("Read operations are not supported").asPromise();
    }

    @Override
    public Promise<ActionResponse, ResourceException> actionInstance(final Context context,
            final ActionRequest request) {
        try {
            switch (request.getActionAsEnum(ConnectorAction.class)) {
            case script:
                return handleScriptAction(request);
            case test:
                return handleTestAction(context, request);
            case livesync:
                return handleLiveSyncAction(context, request);
            default:
                return new BadRequestException("Unsupported action: " + request.getAction()).asPromise();
            }
        } catch (ConnectorException e) {
            return adaptConnectorException(context, request, e, null, request.getResourcePath(), null, null,
                    activityLogger).asPromise();
        } catch (JsonValueException e) {
            return new BadRequestException(e.getMessage(), e).asPromise();
        } catch (IllegalArgumentException e) {
            // from request.getActionAsEnum
            return new BadRequestException(e.getMessage(), e).asPromise();
        } catch (Exception e) {
            return new InternalServerErrorException(e.getMessage(), e).asPromise();
        }
    }

    @Override
    public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) {
        return new NotSupportedException("Patch operations are not supported").asPromise();
    }

    @Override
    public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) {
        return new NotSupportedException("Update operations are not supported").asPromise();
    }

    private Promise<ActionResponse, ResourceException> handleScriptAction(final ActionRequest request) {
        try {
            final String scriptId = request.getAdditionalParameter(SystemAction.SCRIPT_ID);
            if (StringUtils.isBlank(scriptId)) {
                return new BadRequestException("Missing required parameter: " + SystemAction.SCRIPT_ID).asPromise();
            }

            if (!localSystemActionCache.containsKey(scriptId)) {
                return new BadRequestException("Script ID: " + scriptId + " is not defined.").asPromise();
            }

            SystemAction action = localSystemActionCache.get(scriptId);

            String systemType = connectorReference.getConnectorKey().getConnectorName();
            final List<ScriptContextBuilder> scriptContextBuilderList = action.getScriptContextBuilders(systemType);

            if (scriptContextBuilderList.isEmpty()) {
                return new BadRequestException(
                        "Script ID: " + scriptId + " for systemType " + systemType + " is not defined.")
                                .asPromise();
            }

            JsonValue result = new JsonValue(new HashMap<String, Object>());

            boolean onConnector = !"resource"
                    .equalsIgnoreCase(request.getAdditionalParameter(SystemAction.SCRIPT_EXECUTE_MODE));

            final ConnectorFacade facade = getConnectorFacade0(
                    onConnector ? ScriptOnConnectorApiOp.class : ScriptOnResourceApiOp.class);

            String variablePrefix = request.getAdditionalParameter(SystemAction.SCRIPT_VARIABLE_PREFIX);

            List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>(
                    scriptContextBuilderList.size());
            result.put("actions", resultList);

            for (ScriptContextBuilder contextBuilder : scriptContextBuilderList) {
                boolean isShell = contextBuilder.getScriptLanguage().equalsIgnoreCase("Shell");
                for (Entry<String, String> entry : request.getAdditionalParameters().entrySet()) {
                    final String key = entry.getKey();
                    if (SystemAction.SCRIPT_PARAMS.contains(key)) {
                        continue;
                    }
                    Object value = entry.getValue();
                    Object newValue = value;
                    if (isShell) {
                        if ("password".equalsIgnoreCase(key)) {
                            if (value instanceof String) {
                                newValue = new GuardedString(((String) value).toCharArray());
                            } else {
                                return new BadRequestException("Invalid type for password.").asPromise();
                            }
                        }
                        if ("username".equalsIgnoreCase(key)) {
                            if (value instanceof String == false) {
                                return new BadRequestException("Invalid type for username.").asPromise();
                            }
                        }
                        if ("workingdir".equalsIgnoreCase(key)) {
                            if (value instanceof String == false) {
                                return new BadRequestException("Invalid type for workingdir.").asPromise();
                            }
                        }
                        if ("timeout".equalsIgnoreCase(key)) {
                            if (!(value instanceof String) && !(value instanceof Number)) {
                                return new BadRequestException("Invalid type for timeout.").asPromise();
                            }
                        }
                        contextBuilder.addScriptArgument(key, newValue);
                        continue;
                    }

                    if (null != value) {
                        if (value instanceof Collection) {
                            newValue = Array.newInstance(Object.class, ((Collection) value).size());
                            int i = 0;
                            for (Object v : (Collection) value) {
                                if (null == v || FrameworkUtil.isSupportedAttributeType(v.getClass())) {
                                    Array.set(newValue, i, v);
                                } else {
                                    // Serializable may not be
                                    // acceptable
                                    Array.set(newValue, i, v instanceof Serializable ? v : v.toString());
                                }
                                i++;
                            }

                        } else if (value.getClass().isArray()) {
                            // TODO implement the array support later
                        } else if (!FrameworkUtil.isSupportedAttributeType(value.getClass())) {
                            // Serializable may not be acceptable
                            newValue = value instanceof Serializable ? value : value.toString();
                        }
                    }
                    contextBuilder.addScriptArgument(key, newValue);
                }

                JsonValue content = request.getContent();
                // if there is no content(content.isNull()), skip adding content to script arguments
                if (content.isMap()) {
                    for (Entry<String, Object> entry : content.asMap().entrySet()) {
                        contextBuilder.addScriptArgument(entry.getKey(), entry.getValue());
                    }
                } else if (!content.isNull()) {
                    return new BadRequestException("Content is not of type Map").asPromise();
                }

                // ScriptContext scriptContext = script.getScriptContextBuilder().build();
                OperationOptionsBuilder operationOptionsBuilder = new OperationOptionsBuilder();

                // It's necessary to keep the backward compatibility with Waveset IDM
                if (null != variablePrefix && isShell) {
                    operationOptionsBuilder.setOption("variablePrefix", variablePrefix);
                }

                Map<String, Object> actionResult = new HashMap<String, Object>(2);
                try {
                    Object scriptResult = null;
                    if (onConnector) {
                        scriptResult = facade.runScriptOnConnector(contextBuilder.build(),
                                operationOptionsBuilder.build());
                    } else {
                        scriptResult = facade.runScriptOnResource(contextBuilder.build(),
                                operationOptionsBuilder.build());
                    }
                    actionResult.put("result", ConnectorUtil.coercedTypeCasting(scriptResult, Object.class));
                } catch (Throwable t) {
                    logger.error("Script execution error.", t);
                    actionResult.put("error", t.getMessage());
                }
                resultList.add(actionResult);
            }
            return newActionResponse(result).asPromise();
        } catch (ResourceException e) {
            return e.asPromise();
        } catch (Exception e) {
            return new InternalServerErrorException(e.getMessage(), e).asPromise();
        }
    }

    private Promise<ActionResponse, ResourceException> handleTestAction(Context context, ActionRequest request) {
        return newActionResponse(new JsonValue(getStatus(context))).asPromise();
    }

    private Promise<ActionResponse, ResourceException> handleLiveSyncAction(final Context context,
            final ActionRequest request) {

        final String objectTypeName = getObjectTypeName(ObjectClass.ALL);

        if (objectTypeName == null) {
            return new BadRequestException("__ALL__ object class is not configured").asPromise();
        }

        final ActionRequest forwardRequest = Requests.newActionRequest(getSource(objectTypeName),
                request.getAction());

        // forward request to be handled in ObjectClassResourceProvider#actionCollection
        try {
            return connectionFactory.getConnection().action(context, forwardRequest).asPromise();
        } catch (ResourceException e) {
            return e.asPromise();
        }
    }

    /**
     * Checks the {@code operation} permission before execution.
     *
     * @param operation
     * @return if {@code denied} is true and the {@code onDeny} equals
     *         {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#ALLOW}
     *         returns false else true
     * @throws ResourceException
     *             if {@code denied} is true and the {@code onDeny} equals
     *             {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#THROW_EXCEPTION}
     */
    private ConnectorFacade getConnectorFacade0(Class<? extends APIOperation> operation) throws ResourceException {
        final ConnectorFacade facade = getConnectorFacade();
        if (null == facade) {
            throw new ServiceUnavailableException();
        }
        OperationOptionInfoHelper operationOptionInfoHelper = null;

        if (null == facade.getOperation(operation)) {
            throw new NotSupportedException(
                    "Operation " + operation.getCanonicalName() + " is not supported by the Connector");
        } else if (null != operationOptionInfoHelper && OperationOptionInfoHelper.OnActionPolicy.THROW_EXCEPTION
                .equals(operationOptionInfoHelper.getOnActionPolicy())) {
            throw new ForbiddenException(
                    "Operation " + operation.getCanonicalName() + " is configured to be denied");
        }
        return facade;
    }

    private class ObjectClassRequestHandler implements RequestHandler {

        public static final String OBJECTCLASS = "objectclass";
        public static final String OBJECTCLASS_TEMPLATE = "/{objectclass}";

        protected String getObjectClass(Context context) throws ResourceException {
            Map<String, String> variables = ResourceUtil.getUriTemplateVariables(context);
            if (null != variables && variables.containsKey(OBJECTCLASS)) {
                return variables.get(OBJECTCLASS);
            }
            throw new ForbiddenException("Direct access without Router to this service is forbidden.");
        }

        public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleAction(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleCreate(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleDelete(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handlePatch(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<QueryResponse, ResourceException> handleQuery(Context context, QueryRequest request,
                QueryResourceHandler handler) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleQuery(context, request, handler);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleRead(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
            try {
                String objectClass = getObjectClass(context);
                RequestHandler delegate = objectClassHandlers.get(objectClass);
                if (null != delegate) {
                    return delegate.handleUpdate(context, request);
                } else {
                    throw new NotFoundException("Not found: " + objectClass);
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }
    }

    /**
     * ActionRequest actions we support on /system/[systemName]/[objectClass/{id}
     */
    private enum ObjectClassAction {
        authenticate, resolveUsername, liveSync
    }

    /**
     * Handle request on /system/[systemName]/[objectClass]/{id}
     *
     * @ThreadSafe
     */
    private class ObjectClassResourceProvider implements RequestHandler {

        private final ObjectClassInfoHelper objectClassInfoHelper;
        private final Map<Class<? extends APIOperation>, OperationOptionInfoHelper> operations;
        private final String objectClass;

        private ObjectClassResourceProvider(String objectClass, ObjectClassInfoHelper objectClassInfoHelper,
                Map<Class<? extends APIOperation>, OperationOptionInfoHelper> operations) {
            this.objectClassInfoHelper = objectClassInfoHelper;
            this.operations = operations;
            this.objectClass = objectClass;
        }

        /**
         * Checks the {@code operation} permission before execution.
         *
         * @param operation
         * @return if {@code denied} is true and the {@code onDeny} equals
         *         {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#ALLOW}
         *         returns false else true
         * @throws ResourceException
         *             if {@code denied} is true and the {@code onDeny} equals
         *             {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#THROW_EXCEPTION}
         */
        private ConnectorFacade getConnectorFacade0(Class<? extends APIOperation> operation)
                throws ResourceException {
            final ConnectorFacade facade = getConnectorFacade();
            if (null == facade) {
                throw new ServiceUnavailableException();
            }
            OperationOptionInfoHelper operationOptionInfoHelper = operations.get(operation);

            if (null == facade.getOperation(operation)) {
                throw new NotSupportedException(
                        "Operation " + operation.getCanonicalName() + " is not supported by the Connector");
            } else if (null != operationOptionInfoHelper
                    && (null != operationOptionInfoHelper.getSupportedObjectTypes())) {
                if (!operationOptionInfoHelper.getSupportedObjectTypes()
                        .contains(objectClassInfoHelper.getObjectClass().getObjectClassValue())) {
                    throw new NotSupportedException("Actions are not supported for resource instances");
                } else if (OperationOptionInfoHelper.OnActionPolicy.THROW_EXCEPTION
                        .equals(operationOptionInfoHelper.getOnActionPolicy())) {
                    throw new ForbiddenException(
                            "Operation " + operation.getCanonicalName() + " is configured to be denied");
                }
            }
            return facade;
        }

        private Promise<ActionResponse, ResourceException> handleAuthenticate(Context context,
                ActionRequest request) throws IOException {
            try {
                final ConnectorFacade facade = getConnectorFacade0(AuthenticationApiOp.class);
                final JsonValue params = new JsonValue(request.getAdditionalParameters());
                final String username = params.get("username").required().asString();
                final String password = params.get("password").required().asString();

                OperationOptions operationOptions = operations.get(AuthenticationApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper).build();

                // Throw ConnectorException
                Uid uid = facade.authenticate(objectClassInfoHelper.getObjectClass(), username,
                        new GuardedString(password.toCharArray()), operationOptions);

                JsonValue result = new JsonValue(new HashMap<String, Object>());
                result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue());
                if (null != uid.getRevision()) {
                    result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision());
                }
                return newActionResponse(result).asPromise();
            } catch (ConnectorException e) {
                // handle ConnectorException from facade.authenticate:
                // log to activity log only if this is an external request
                // (let internal requests do their own logging upon the handleError...)
                throw adaptConnectorException(context, request, e, null, null, null, null,
                        ContextUtil.isExternal(context) ? activityLogger : NullActivityLogger.INSTANCE);
            }
        }

        private Promise<ActionResponse, ResourceException> handleLiveSync(Context context, ActionRequest request)
                throws ResourceException {
            final ActionRequest forwardRequest = Requests
                    .newActionRequest(ProvisionerService.ROUTER_PREFIX, request.getAction())
                    .setAdditionalParameter("source", getSource(objectClass));

            // forward request to be handled in SystemObjectSetService#actionInstance
            return connectionFactory.getConnection().action(context, forwardRequest).asPromise();

        }

        public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
            try {
                switch (request.getActionAsEnum(ObjectClassAction.class)) {
                case authenticate:
                    return handleAuthenticate(context, request);
                case liveSync:
                    return handleLiveSync(context, request);
                default:
                    throw new BadRequestException("Unsupported action: " + request.getAction());
                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (IllegalArgumentException e) { // from request.getActionAsEnum
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
            try {
                final ConnectorFacade facade = getConnectorFacade0(CreateApiOp.class);
                final Set<Attribute> createAttributes = objectClassInfoHelper.getCreateAttributes(request,
                        cryptoService);

                OperationOptions operationOptions = operations.get(CreateApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper).build();

                Uid uid = facade.create(objectClassInfoHelper.getObjectClass(),
                        AttributeUtil.filterUid(createAttributes), operationOptions);

                ResourceResponse resource = getCurrentResource(facade, uid, null);
                activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), null,
                        resource.getContent(), Status.SUCCESS);
                return resource.asPromise();
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, getSource(objectClass),
                        objectClassInfoHelper.getFullResourceId(request), request.getContent(), null,
                        activityLogger).asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        @Override
        public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
            String resourceId = objectClassInfoHelper.getFullResourceId(request);
            try {
                if (resourceId.isEmpty()) {
                    throw new BadRequestException(
                            "The resource collection " + request.getResourcePath() + " cannot be deleted");
                }
                final ConnectorFacade facade = getConnectorFacade0(DeleteApiOp.class);
                final Uid uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision())
                        : new Uid(resourceId);

                // do a read first (largely for logging)
                ResourceResponse before = getCurrentResource(facade, uid, null);

                OperationOptions operationOptions = operations.get(DeleteApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper).build();

                facade.delete(objectClassInfoHelper.getObjectClass(), uid, operationOptions);

                JsonValue result = before.getContent().copy();
                result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue());
                if (null != uid.getRevision()) {
                    result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision());
                }
                activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()),
                        before.getContent(), null, Status.SUCCESS);
                return newResourceResponse(uid.getUidValue(), uid.getRevision(), result).asPromise();
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, null, null,
                        activityLogger).asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        @Override
        public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
            JsonValue beforeValue = null;
            String resourceId = objectClassInfoHelper.getFullResourceId(request);
            try {
                if (resourceId.isEmpty()) {
                    throw new BadRequestException(
                            "The resource collection " + request.getResourcePath() + " cannot be patched");
                }

                final ConnectorFacade facade = getConnectorFacade0(UpdateApiOp.class);
                final Uid _uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision())
                        : new Uid(resourceId);

                // read resource before update for logging
                ResourceResponse before = getCurrentResource(facade, _uid, null);
                beforeValue = before.getContent();

                final Set<String> attributeNames = new HashSet<String>();
                final Set<Attribute> addedAttributes = new HashSet<Attribute>();
                final Set<Attribute> removedAttributes = new HashSet<Attribute>();
                final Set<Attribute> updatedAttributes = new HashSet<Attribute>();
                for (PatchOperation operation : request.getPatchOperations()) {
                    Attribute attribute = objectClassInfoHelper.getPatchAttribute(operation, beforeValue,
                            cryptoService);
                    if (attribute != null) {
                        if (operation.isAdd()) {
                            addedAttributes.add(attribute);
                        } else if (operation.isRemove()) {
                            removedAttributes.add(attribute);
                        } else {
                            updatedAttributes.add(attribute);
                        }
                        attributeNames.add(attribute.getName());
                    }
                }

                OperationOptions operationOptions;
                OperationOptionsBuilder operationOptionsBuilder = operations.get(UpdateApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper);

                final String reauthPassword = getReauthPassword(context);

                // if reauth and updating attribute requiring user credentials
                if (runAsUser(attributeNames, reauthPassword)) {
                    // get username attribute
                    final List<String> usernameAttrs = jsonConfiguration
                            .get(ConnectorUtil.OPENICF_CONFIGURATION_PROPERTIES).get(ACCOUNT_USERNAME_ATTRIBUTES)
                            .asList(String.class);
                    final String username = beforeValue.get(usernameAttrs.get(0)).asString();

                    if (StringUtils.isNotBlank(username)) {
                        operationOptionsBuilder.setRunAsUser(username)
                                .setRunWithPassword(new GuardedString(reauthPassword.toCharArray()));
                    }
                }

                operationOptions = operationOptionsBuilder.build();

                Uid uid = _uid;
                if (addedAttributes.size() > 0) {
                    // Perform any add operations
                    uid = facade.addAttributeValues(objectClassInfoHelper.getObjectClass(), uid,
                            AttributeUtil.filterUid(addedAttributes), operationOptions);
                }
                if (removedAttributes.size() > 0) {
                    // Perform any remove operations
                    try {
                        uid = facade.removeAttributeValues(objectClassInfoHelper.getObjectClass(), uid,
                                AttributeUtil.filterUid(removedAttributes), operationOptions);
                    } catch (ConnectorException e) {
                        logger.debug("Error removing attribute values for object {}", uid, e);
                    }
                }
                if (updatedAttributes.size() > 0) {
                    // Perform any increment or replace operations
                    uid = facade.update(objectClassInfoHelper.getObjectClass(), uid,
                            AttributeUtil.filterUid(updatedAttributes), operationOptions);
                }

                ResourceResponse resource = getCurrentResource(facade, uid, null);
                activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()),
                        beforeValue, resource.getContent(), Status.SUCCESS);
                return resource.asPromise();
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, beforeValue,
                        null, activityLogger).asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context,
                final QueryRequest request, final QueryResourceHandler handler) {
            EventEntry measure = Publisher.start(getQueryEventName(objectClass, request), request, null);
            String resourceId = objectClassInfoHelper.getFullResourceId(request);
            try {
                if (!resourceId.isEmpty()) {
                    throw new BadRequestException(
                            "The resource instance " + request.getResourcePath() + " cannot be queried");
                }

                final ConnectorFacade facade = getConnectorFacade0(SearchApiOp.class);
                OperationOptionsBuilder operationOptionsBuilder = operations.get(SearchApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper);

                Filter filter = null;

                if (request.getQueryId() != null) {
                    if (ServerConstants.QUERY_ALL_IDS.equals(request.getQueryId())) {
                        operationOptionsBuilder.setAttributesToGet(Uid.NAME);
                    } else {
                        throw new BadRequestException("Unsupported _queryId: " + request.getQueryId());
                    }
                } else if (request.getQueryExpression() != null) {
                    filter = QueryFilters.parse(request.getQueryExpression()).accept(RESOURCE_FILTER,
                            objectClassInfoHelper);

                } else if (request.getQueryFilter() != null) {
                    // No filtering or query by filter.
                    filter = request.getQueryFilter().accept(RESOURCE_FILTER, objectClassInfoHelper);
                } else {
                    throw new BadRequestException(
                            "One of _queryId, _queryExpression, or _queryFilter is required.");
                }

                // If paged results are requested then decode the cookie in
                // order to determine
                // the index of the first result to be returned.
                final int pageSize = request.getPageSize();
                final String pagedResultsCookie = request.getPagedResultsCookie();
                final boolean pagedResultsRequested = request.getPageSize() > 0;
                if (pageSize > 0) {
                    operationOptionsBuilder.setPageSize(pageSize);
                }
                if (null != pagedResultsCookie) {
                    operationOptionsBuilder.setPagedResultsCookie(pagedResultsCookie);
                }
                operationOptionsBuilder.setPagedResultsOffset(request.getPagedResultsOffset());
                if (null != request.getSortKeys()) {
                    List<SortKey> sortKeys = new ArrayList<SortKey>(request.getSortKeys().size());
                    for (org.forgerock.json.resource.SortKey s : request.getSortKeys()) {
                        sortKeys.add(new SortKey(s.getField().leaf(), s.isAscendingOrder()));
                    }
                    operationOptionsBuilder.setSortKeys(sortKeys);
                }

                // Override ATTRS_TO_GET if fields are specified within the Request
                if (!request.getFields().isEmpty()) {
                    objectClassInfoHelper.setAttributesToGet(operationOptionsBuilder, request.getFields());
                }

                final JsonValue logValue = json(array());
                final Exception[] ex = new Exception[] { null };
                SearchResult searchResult = facade.search(objectClassInfoHelper.getObjectClass(), filter,
                        new ResultsHandler() {
                            @Override
                            public boolean handle(ConnectorObject obj) {
                                try {
                                    ResourceResponse resource = objectClassInfoHelper.build(obj, cryptoService);
                                    logValue.add(resource.getContent().getObject());
                                    return handler.handleResource(resource);
                                } catch (Exception e) {
                                    ex[0] = e;
                                    // TODO ICF needs a way to handle exceptions through the facade
                                    return false;
                                }
                            }
                        }, operationOptionsBuilder.build());
                if (ex[0] != null) {
                    throw new InternalServerErrorException(ex[0].getMessage(), ex[0]);
                }
                activityLogger.log(context, request,
                        "query: " + request.getQueryId() + ", queryExpression: " + request.getQueryExpression()
                                + ", queryFilter: "
                                + (request.getQueryFilter() != null ? request.getQueryFilter().toString() : null)
                                + ", parameters: " + request.getAdditionalParameters(),
                        request.getQueryId(), null, logValue, Status.SUCCESS);

                // TODO-crest3- fix contract for remainingPagedResults
                return newResultPromise(
                        newQueryResponse(searchResult != null ? searchResult.getPagedResultsCookie() : null));
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, null, null, null, null, activityLogger)
                        .asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (AttributeMissingException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (IllegalArgumentException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            } finally {
                measure.end();
            }
        }

        @Override
        public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
            String resourceId = objectClassInfoHelper.getFullResourceId(request);
            try {
                if (resourceId.isEmpty()) {
                    throw new BadRequestException(
                            "The resource collection " + request.getResourcePath() + " cannot be read");
                }

                final ConnectorFacade facade = getConnectorFacade0(GetApiOp.class);
                Uid uid = new Uid(resourceId);
                ConnectorObject connectorObject = getConnectorObject(facade, uid, request.getFields());

                if (null != connectorObject) {
                    ResourceResponse resource = objectClassInfoHelper.build(connectorObject, cryptoService);
                    activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()),
                            resource.getContent(), resource.getContent(), Status.SUCCESS);
                    return resource.asPromise();
                } else {
                    final String matchedUri = context.containsContext(UriRouterContext.class)
                            ? context.asContext(UriRouterContext.class).getMatchedUri()
                            : "unknown path";
                    throw new NotFoundException("Object " + resourceId + " not found on " + matchedUri);

                }
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, null, null,
                        activityLogger).asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        @Override
        public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
            JsonValue content = request.getContent();
            String resourceId = objectClassInfoHelper.getFullResourceId(request);
            try {
                if (resourceId.isEmpty()) {
                    throw new BadRequestException(
                            "The resource collection " + request.getResourcePath() + " cannot be updated");
                }

                final ConnectorFacade facade = getConnectorFacade0(UpdateApiOp.class);
                final Uid _uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision())
                        : new Uid(resourceId);

                // read resource before update for logging
                ResourceResponse before = getCurrentResource(facade, _uid, null);

                // TODO Fix for http://bugster.forgerock.org/jira/browse/CREST-29
                final Name newName = null;
                final Set<Attribute> replaceAttributes = objectClassInfoHelper.getUpdateAttributes(request, newName,
                        cryptoService);

                OperationOptions operationOptions;
                OperationOptionsBuilder operationOptionsBuilder = operations.get(UpdateApiOp.class)
                        .build(jsonConfiguration, objectClassInfoHelper);

                final String reauthPassword = getReauthPassword(context);

                // if reauth and updating attributes requiring user credentials
                if (runAsUser(content.asMap().keySet(), reauthPassword)) {
                    // get username attribute
                    final List<String> usernameAttrs = jsonConfiguration
                            .get(ConnectorUtil.OPENICF_CONFIGURATION_PROPERTIES).get(ACCOUNT_USERNAME_ATTRIBUTES)
                            .asList(String.class);
                    final String username = content.get(usernameAttrs.get(0)).asString();

                    if (StringUtils.isNotBlank(username)) {
                        operationOptionsBuilder.setRunAsUser(username)
                                .setRunWithPassword(new GuardedString(reauthPassword.toCharArray()));
                    }
                }

                operationOptions = operationOptionsBuilder.build();

                Uid uid = facade.update(objectClassInfoHelper.getObjectClass(), _uid,
                        AttributeUtil.filterUid(replaceAttributes), operationOptions);

                ResourceResponse resource = getCurrentResource(facade, uid, null);
                activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()),
                        before.getContent(), resource.getContent(), Status.SUCCESS);
                return resource.asPromise();
            } catch (ResourceException e) {
                return e.asPromise();
            } catch (ConnectorException e) {
                return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, content,
                        null, activityLogger).asPromise();
            } catch (JsonValueException e) {
                return new BadRequestException(e.getMessage(), e).asPromise();
            } catch (Exception e) {
                return new InternalServerErrorException(e.getMessage(), e).asPromise();
            }
        }

        // see if there is a reauth password provided
        private String getReauthPassword(Context context) {
            try {
                // get reauth password
                return context.asContext(HttpContext.class).getHeaderAsString(REAUTH_HEADER);
            } catch (Exception e) {
                // there will not always be a HttpContext and this is acceptable so catch exception to
                // prevent the exception from  stopping the remaining update
                return null;
            }
        }

        /**
         * Checks if any of the supplied attributes require re-authentication to update.
         * 
         * @param attributes the attributes being updated
         * @param reauthPassword the re-authentication password
         * @return true if a password is re-authentication is required, false otherwise.
         */
        private boolean runAsUser(Set<String> attributes, String reauthPassword) {
            final JsonValue properties = objectClassInfoHelper.getProperties();
            final Predicate<String> attributesToRunAsUser = new Predicate<String>() {
                @Override
                public boolean apply(String attribute) {
                    return !properties.get(attribute).isNull()
                            && properties.get(attribute).get(RUN_AS_USER).defaultTo(false).asBoolean();
                }
            };

            return StringUtils.isNotEmpty(reauthPassword)
                    && FluentIterable.from(attributes).filter(attributesToRunAsUser).iterator().hasNext();
        }

        private ResourceResponse getCurrentResource(final ConnectorFacade facade, final Uid uid,
                final List<JsonPointer> fields) throws IOException, JsonCryptoException {

            final ConnectorObject co = getConnectorObject(facade, uid, fields);
            if (null != co) {
                return objectClassInfoHelper.build(co, cryptoService);
            } else {
                JsonValue result = new JsonValue(new HashMap<String, Object>());
                result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue());
                if (null != uid.getRevision()) {
                    result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision());
                }
                return newResourceResponse(uid.getUidValue(), uid.getRevision(), result);
            }
        }

        private ConnectorObject getConnectorObject(final ConnectorFacade facade, final Uid uid,
                final List<JsonPointer> fields) throws IOException, JsonCryptoException {

            final OperationOptions operationOptions;
            if (fields == null || fields.isEmpty()) {
                operationOptions = operations.get(GetApiOp.class).build(jsonConfiguration, objectClassInfoHelper)
                        .build();
            } else {
                OperationOptionsBuilder operationOptionsBuilder = new OperationOptionsBuilder();
                objectClassInfoHelper.setAttributesToGet(operationOptionsBuilder, fields);
                operationOptions = operationOptionsBuilder.build();
            }

            return facade.getObject(objectClassInfoHelper.getObjectClass(), uid, operationOptions);
        }
    }

    /**
     * Handle request on /system/[systemName]/schema
     *
     * @ThreadSafe
     */
    private static class SchemaResourceProvider implements SingletonResourceProvider {

        private final ResourceResponse schema;

        private SchemaResourceProvider(ResourceResponse schema) {
            this.schema = schema;
        }

        @Override
        public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) {
            return schema.asPromise();
        }

        @Override
        public Promise<ActionResponse, ResourceException> actionInstance(Context context, ActionRequest request) {
            return new NotSupportedException("Actions are not supported for resource instances").asPromise();
        }

        @Override
        public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) {
            return new NotSupportedException("Patch operations are not supported").asPromise();
        }

        @Override
        public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) {
            return new NotSupportedException("Update operations are not supported").asPromise();
        }
    }

    /**
     * A container for information about a sync retry after a failure
     */
    private class SyncRetry {

        /**
         * The retry value, true if the sync should be retried, false otherwise.
         */
        boolean value;

        /**
         * The {@link Throwable} associated with the failure
         */
        Throwable throwable;

        public SyncRetry() {
            value = false;
            throwable = null;
        }

        /**
         * Returns the retry value.
         * 
         * @return true if the sync should be retried, false otherwise.
         */
        public boolean getValue() {
            return value;
        }

        /**
         * Sets the retry value.
         * 
         * @param value true if the sync should be retried, false otherwise.
         */
        public void setValue(boolean value) {
            this.value = value;
        }

        /**
         * Returns the {@link Throwable} associated with the failure
         * 
         * @return the {@link Throwable} associated with the failure
         */
        public Throwable getThrowable() {
            return throwable;
        }

        /**
         * Sets the {@link Throwable} associated with the failure.
         * 
         * @param throwable the {@link Throwable} associated with the failure.
         */
        public void setThrowable(Throwable throwable) {
            this.throwable = throwable;
        }
    }

    private static final QueryFilterVisitor<Filter, ObjectClassInfoHelper, JsonPointer> RESOURCE_FILTER = new QueryFilterVisitor<Filter, ObjectClassInfoHelper, JsonPointer>() {

        @Override
        public Filter visitAndFilter(final ObjectClassInfoHelper helper,
                List<QueryFilter<JsonPointer>> subFilters) {
            final Iterator<QueryFilter<JsonPointer>> iterator = subFilters.iterator();
            if (iterator.hasNext()) {
                return buildAnd(helper, iterator.next(), iterator);
            } else {
                throw new IllegalArgumentException("cannot parse 'and' QueryFilter with zero operands");
            }
        }

        private Filter buildAnd(final ObjectClassInfoHelper helper, final QueryFilter<JsonPointer> left,
                final Iterator<QueryFilter<JsonPointer>> iterator) {
            if (iterator.hasNext()) {
                final QueryFilter<JsonPointer> right = iterator.next();
                return and(left.accept(this, helper), buildAnd(helper, right, iterator));
            } else {
                return left.accept(this, helper);
            }
        }

        @Override
        public Filter visitOrFilter(ObjectClassInfoHelper helper, List<QueryFilter<JsonPointer>> subFilters) {
            final Iterator<QueryFilter<JsonPointer>> iterator = subFilters.iterator();
            if (iterator.hasNext()) {
                return buildOr(helper, iterator.next(), iterator);
            } else {
                throw new IllegalArgumentException("cannot parse 'or' QueryFilter with zero operands");
            }
        }

        private Filter buildOr(final ObjectClassInfoHelper helper, final QueryFilter<JsonPointer> left,
                final Iterator<QueryFilter<JsonPointer>> iterator) {
            if (iterator.hasNext()) {
                final QueryFilter<JsonPointer> right = iterator.next();
                return or(left.accept(this, helper), buildOr(helper, right, iterator));
            } else {
                return left.accept(this, helper);
            }
        }

        @Override
        public Filter visitBooleanLiteralFilter(final ObjectClassInfoHelper helper, final boolean value) {
            return new Filter() {
                public boolean accept(ConnectorObject obj) {
                    return value;
                }

                public <R extends Object, P extends Object> R accept(
                        org.identityconnectors.framework.common.objects.filter.FilterVisitor<R, P> v, P p) {
                    // OpenICF team explained that
                    // return v.visitExtendedFilter(p, this);
                    // would not yet (1.4) work with all connectors and/or remotely.
                    // Instead the return null evaluates to always true.       
                    if (value) {
                        return null; // OpenICF contract evaluates null return to always true
                    } else {
                        throw new UnsupportedOperationException(
                                "visitBooleanLiteralFilter only supported for literal true, not false");
                    }
                }
            };
        }

        @Override
        public Filter visitContainsFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) {
            return contains(helper.filterAttribute(field, valueAssertion));
        }

        @Override
        public Filter visitEqualsFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) {
            return equalTo(helper.filterAttribute(field, valueAssertion));
        }

        /**
         * EndsWith filter
         */
        private static final String EW = "ew";
        /**
         * ContainsAll filter
         */
        private static final String CA = "ca";

        @Override
        public Filter visitExtendedMatchFilter(ObjectClassInfoHelper helper, JsonPointer field,
                String matchingRuleId, Object valueAssertion) {
            if (EW.equals(matchingRuleId)) {
                return endsWith(helper.filterAttribute(field, valueAssertion));
            } else if (CA.equals(matchingRuleId)) {
                return containsAllValues(helper.filterAttribute(field, valueAssertion));
            }
            throw new IllegalArgumentException("ExtendedMatchFilter is not supported");
        }

        @Override
        public Filter visitGreaterThanFilter(ObjectClassInfoHelper helper, JsonPointer field,
                Object valueAssertion) {
            return greaterThan(helper.filterAttribute(field, valueAssertion));
        }

        @Override
        public Filter visitGreaterThanOrEqualToFilter(ObjectClassInfoHelper helper, JsonPointer field,
                Object valueAssertion) {
            return greaterThanOrEqualTo(helper.filterAttribute(field, valueAssertion));
        }

        @Override
        public Filter visitLessThanFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) {
            return lessThan(helper.filterAttribute(field, valueAssertion));
        }

        @Override
        public Filter visitLessThanOrEqualToFilter(ObjectClassInfoHelper helper, JsonPointer field,
                Object valueAssertion) {
            return lessThanOrEqualTo(helper.filterAttribute(field, valueAssertion));
        }

        @Override
        public Filter visitNotFilter(ObjectClassInfoHelper helper, QueryFilter<JsonPointer> subFilter) {
            return not(subFilter.accept(this, helper));
        }

        @Override
        public Filter visitPresentFilter(ObjectClassInfoHelper helper, JsonPointer field) {
            throw new IllegalArgumentException("PresentFilter is not supported");
        }

        @Override
        public Filter visitStartsWithFilter(ObjectClassInfoHelper helper, JsonPointer field,
                Object valueAssertion) {
            return startsWith(helper.filterAttribute(field, valueAssertion));
        }
    };

    /**
     * Gets the unique {@link org.forgerock.openidm.provisioner.SystemIdentifier} of this instance.
     * <p/>
     * The service which refers to this service instance can distinguish between multiple instances by this value.
     *
     * @return
     */
    public SystemIdentifier getSystemIdentifier() {
        return systemIdentifier;
    }

    public String getSystemIdentifierName() {
        return systemIdentifier.getName();
    }

    /**
     * Gets the fully qualified path name
     * @param objectClass the object class for the intended resource
     * @param optionalId ids to append to the fully qualified path
     * @return
     */
    private String getSource(final String objectClass, final String... optionalId) {
        final StringBuilder sb = new StringBuilder("system").append("/").append(systemIdentifier.getName())
                .append("/").append(objectClass);
        for (String id : optionalId) {
            sb.append("/").append(id);
        }
        return sb.toString();
    }

    /**
     * Gets a brief status report about the current status of this service instance.
     * <p>
     * An example response when the configuration is enabled
     * {@code {
     * "name" : "ldap",
     * "enabled" : true,
     * "config" : "config/provisioner.openicf/ldap"
     * "objectTypes":
     * [
     *  "group",
     *  "account"
     * ],
     * "connectorRef" :
     *  {
     *      "connectorName": "org.identityconnectors.ldap.LdapConnector",
     *      "bundleName": "org.forgerock.openicf.connectors.ldap-connector",
     *      "bundleVersion": "[1.1.0.1,1.1.2.0)"
     *  } ,
     * "ok" : true
     * }}
     *
     * An example response when the configuration is disabled
     * {@code {
     * "name": "ldap",
     * "enabled": false,
     * "config": "config/provisioner.openicf/ldap",
     * "objectTypes":
     * [
     *  "group",
     *  "account"
     * ],
     * "connectorRef":
     * {
     *      "connectorName": "org.identityconnectors.ldap.LdapConnector",
     *      "bundleName": "org.forgerock.openicf.connectors.ldap-connector",
     *      "bundleVersion": "[1.4.0.0,2.0.0.0)"
     * },
     * "error": "connector not available",
     * "ok": false
     * }}
     *
     * @param context the Context of the request requesting the status
     * @return a Map of the current status of a connector
     */
    public Map<String, Object> getStatus(Context context) {
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        JsonValue jv = new JsonValue(result);
        boolean ok = false;

        jv.put("name", systemIdentifier.getName());
        jv.put("enabled", jsonConfiguration.get("enabled").defaultTo(Boolean.TRUE).asBoolean());
        jv.put("config", "config/provisioner.openicf/" + factoryPid);
        jv.put("objectTypes", ConnectorUtil.getObjectTypes(jsonConfiguration).keySet());
        ConnectorReference connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration);
        if (connectorReference != null) {
            jv.put(ConnectorUtil.OPENICF_CONNECTOR_REF,
                    ConnectorUtil.getConnectorKey(connectorReference.getConnectorKey()));
            ConnectorInfo connectorInfo = connectorInfoProvider.findConnectorInfo(connectorReference);
            if (connectorInfo != null) {
                jv.put("displayName", connectorInfo.getConnectorDisplayName());
            }
        }

        try {
            ConnectorFacade connectorFacade = getConnectorFacade();
            if (connectorFacade == null) {
                jv.put("error", "connector not available");
            } else {
                connectorFacade.test();
                ok = true;
            }
        } catch (UnsupportedOperationException e) {
            jv.put("error", "TEST UnsupportedOperation");
        } catch (InvalidCredentialException e) {
            jv.put("error", "Connection Error");
        } catch (Exception e) {
            jv.put("error", e.getMessage());
        }

        jv.put("ok", ok);
        return result;
    }

    public Map<String, Object> testConfig(JsonValue config) {
        JsonValue jv = json(object());
        jv.put("name", systemIdentifier.getName());
        jv.put("ok", false);
        SimpleSystemIdentifier testIdentifier = null;
        ConnectorReference connectorReference = null;
        try {
            testIdentifier = new SimpleSystemIdentifier(config);
            connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration);
        } catch (JsonValueException e) {
            jv.put("error", "OpenICF Provisioner Service jsonConfiguration has errors: " + e.getMessage());
            return jv.asMap();
        }

        ConnectorInfo connectorInfo = connectorInfoProvider.findConnectorInfo(connectorReference);
        if (null != connectorInfo) {
            ConnectorFacade facade = null;
            try {
                OperationHelperBuilder ohb = new OperationHelperBuilder(testIdentifier.getName(), config,
                        connectorInfo.createDefaultAPIConfiguration(), cryptoService);
                ConnectorFacadeFactory connectorFacadeFactory = ConnectorFacadeFactory.getInstance();
                facade = connectorFacadeFactory.newInstance(ohb.getRuntimeAPIConfiguration());
            } catch (Exception e) {
                jv.put("error", "OpenICF connector jsonConfiguration has errors: " + e.getMessage());
                return jv.asMap();
            }

            if (null != facade && facade.getSupportedOperations().contains(TestApiOp.class)) {
                try {
                    facade.test();
                } catch (UnsupportedOperationException e) {
                    jv.put("reason", "TEST UnsupportedOperation");
                } catch (InvalidCredentialException e) {
                    jv.put("error", "Connection Error");
                } catch (Throwable e) {
                    jv.put("error", e.getMessage());
                    return jv.asMap();
                }
                jv.put("ok", true);
            } else if (null == facade) {
                jv.put("error", "OpenICF ConnectorFacade of " + connectorReference + " is not available");
            } else {
                jv.put("error", "OpenICF connector of " + connectorReference + " does not support test.");
            }
        } else if (connectorReference.getConnectorLocation().equals(ConnectorReference.ConnectorLocation.LOCAL)) {
            jv.put("error", "OpenICF ConnectorInfo can not be loaded for " + connectorReference + " from #LOCAL");
        } else {
            jv.put("error", "OpenICF ConnectorInfo for " + connectorReference + " is not available yet.");
        }
        return jv.asMap();
    }

    /**
     * Get the corresponding object type name from provisioner config
     * @param objectClass the objectClass to get the name of
     * @return the name of the objectClass or null if not found
     */
    protected String getObjectTypeName(final ObjectClass objectClass) {
        if (objectClass == null) {
            return null;
        }
        final Predicate<Entry<String, ObjectClassInfoHelper>> objectClassFilter = new Predicate<Entry<String, ObjectClassInfoHelper>>() {
            public boolean apply(Entry<String, ObjectClassInfoHelper> entry) {
                return objectClass.equals(entry.getValue().getObjectClass());
            }
        };

        final Iterable<Entry<String, ObjectClassInfoHelper>> objectClasses = FluentIterable
                .from(objectTypes.entrySet()).filter(objectClassFilter);

        return objectClasses.iterator().hasNext() ? objectClasses.iterator().next().getKey() : null;
    }

    /**
     * This newBuilder and this method can not be scheduled. The call MUST go
     * through the {@code org.forgerock.openidm.provisioner}
     * <p/>
     * Invoked by the scheduler when the scheduler triggers.
     * <p/>
     * Synchronization object: {@code "connectorData" : "syncToken" :
     * "1305555929000", "nativeType" : "JAVA_TYPE_LONG" },
     * "synchronizationStatus" : { "errorStatus" : null, "lastKnownServer" :
     * "localServer", "lastModDate" : "2011-05-16T14:47:58.587Z", "lastModNum" :
     * 668, "lastPollDate" : "2011-05-16T14:47:52.875Z", "lastStartTime" :
     * "2011-05-16T14:29:07.863Z", "progressMessage" : "SUCCEEDED" } }}
     * <p/>
     * {@inheritDoc} Synchronize the changes from the end system for the given
     * {@code objectType}.
     * <p/>
     * OpenIDM takes active role in the synchronization process by asking the
     * end system to get all changed object. Not all systems are capable to
     * fulfill this kind of request but if the end system is capable then the
     * implementation sends each change to a new request on the router and when
     * it is finished, it returns a new <b>stage</b> object.
     * <p/>
     * The {@code previousStage} object is the previously returned value of this
     * method.
     *
     * @param context the request context associated with the invocation
     * @param previousStage
     *            The previously returned object. If null then it's the first
     *            execution.
     * @return The new updated stage object. This will be the
     *         {@code previousStage} at buildNext call.
     * @throws IllegalArgumentException
     *             if the value of {@code connectorData} can not be converted to
     *             {@link SyncToken}.
     * @throws UnsupportedOperationException
     *             if the {@link SyncApiOp} operation is not implemented in
     *             connector.
     * @throws org.forgerock.json.JsonValueException
     *             if the {@code previousStage} is not Map.
     * @see {@link ConnectorUtil#convertToSyncToken(org.forgerock.json.JsonValue)}
     *      or any exception happed inside the connector.
     */
    public JsonValue liveSynchronize(final Context context, final String objectType, final JsonValue previousStage)
            throws ResourceException {

        if (!serviceAvailable) {
            return previousStage;
        }

        final JsonValue stage = previousStage != null ? previousStage.copy()
                : new JsonValue(new LinkedHashMap<String, Object>());

        JsonValue connectorData = stage.get("connectorData");
        SyncToken token = null;
        if (!connectorData.isNull()) {
            if (connectorData.isMap()) {
                token = ConnectorUtil.convertToSyncToken(connectorData);
            } else {
                throw new IllegalArgumentException("Illegal connectorData property. Value must be Map");
            }
        }
        stage.remove("lastException");

        try {
            final SyncRetry syncRetry = new SyncRetry();
            final OperationHelper helper = operationHelperBuilder.build(objectType, stage, cryptoService);

            if (helper.isOperationPermitted(SyncApiOp.class)) {
                ConnectorFacade connector = getConnectorFacade();
                SyncApiOp operation = (SyncApiOp) connector.getOperation(SyncApiOp.class);
                if (null == operation) {
                    throw new UnsupportedOperationException(SyncApiOp.class.getCanonicalName());
                }
                if (null == token) {
                    token = operation.getLatestSyncToken(helper.getObjectClass());
                    logger.debug("New LatestSyncToken has been fetched. New token is: {}", token);
                } else {
                    final SyncToken[] lastToken = new SyncToken[] { token };
                    final String[] failedRecord = new String[1];
                    OperationOptionsBuilder operationOptionsBuilder = helper
                            .getOperationOptionsBuilder(SyncApiOp.class, null, previousStage);

                    try {
                        logger.debug("Execute sync(ObjectClass:{}, SyncToken:{})",
                                new Object[] { helper.getObjectClass().getObjectClassValue(), token });
                        SyncToken syncToken = operation.sync(helper.getObjectClass(), token,
                                new SyncResultsHandler() {
                                    /**
                                     * Called to handle a delta in the stream. The Connector framework will call
                                     * this method multiple times, once for each result.
                                     * Although this method is callback, the framework will invoke it synchronously.
                                     * Thus, the framework guarantees that once an application's call to
                                     * {@link org.identityconnectors.framework.api.operations.SyncApiOp#sync(org.identityconnectors.framework.common.objects.ObjectClass, org.identityconnectors.framework.common.objects.SyncToken, org.identityconnectors.framework.common.objects.SyncResultsHandler, org.identityconnectors.framework.common.objects.OperationOptions)} SyncApiOp#sync() returns,
                                     * the framework will no longer call this method
                                     * to handle results from that <code>sync()</code> operation.
                                     *
                                     * @param syncDelta The change
                                     * @return True iff the application wants to continue processing more results.
                                     * @throws RuntimeException If the application encounters an exception. This will
                                     * stop iteration and the exception will propagate to the application.
                                     */
                                    @SuppressWarnings("fallthrough")
                                    public boolean handle(SyncDelta syncDelta) {
                                        try {
                                            // Q: are we going to encode ids?
                                            final String resourceId = syncDelta.getUid().getUidValue();
                                            final String objectTypeName = getObjectTypeName(
                                                    syncDelta.getObjectClass());
                                            final String resourceContainer = getSource(
                                                    objectTypeName == null ? objectType : objectTypeName);
                                            final JsonValue content = new JsonValue(
                                                    new LinkedHashMap<String, Object>(2));

                                            //rebuild the OperationHelper if the helper is for the __ALL__ object class
                                            final OperationHelper syncDeltaOperationHelper = helper.getObjectClass()
                                                    .equals(ObjectClass.ALL)
                                                            ? operationHelperBuilder.build(objectTypeName, stage,
                                                                    cryptoService)
                                                            : helper;

                                            switch (syncDelta.getDeltaType()) {
                                            case CREATE: {
                                                JsonValue deltaObject = syncDeltaOperationHelper
                                                        .build(syncDelta.getObject());
                                                content.put("oldValue", null);
                                                content.put("newValue", deltaObject.getObject());
                                                // TODO import SynchronizationService.Action.notifyCreate and ACTION_PARAM_ constants
                                                ActionRequest onCreateRequest = Requests
                                                        .newActionRequest("sync", "notifyCreate")
                                                        .setAdditionalParameter("resourceContainer",
                                                                resourceContainer)
                                                        .setAdditionalParameter("resourceId", resourceId)
                                                        .setContent(content);
                                                connectionFactory.getConnection().action(context, onCreateRequest);

                                                activityLogger.log(context, onCreateRequest, "sync-create",
                                                        onCreateRequest.getResourcePath(), deltaObject, deltaObject,
                                                        Status.SUCCESS);
                                                break;
                                            }
                                            case UPDATE:
                                            case CREATE_OR_UPDATE: {
                                                JsonValue deltaObject = syncDeltaOperationHelper
                                                        .build(syncDelta.getObject());
                                                content.put("oldValue", null);
                                                content.put("newValue", deltaObject.getObject());
                                                if (null != syncDelta.getPreviousUid()) {
                                                    deltaObject.put("_previous-id",
                                                            syncDelta.getPreviousUid().getUidValue());
                                                }
                                                // TODO import SynchronizationService.Action.notifyUpdate and ACTION_PARAM_ constants
                                                ActionRequest onUpdateRequest = Requests
                                                        .newActionRequest("sync", "notifyUpdate")
                                                        .setAdditionalParameter("resourceContainer",
                                                                resourceContainer)
                                                        .setAdditionalParameter("resourceId", resourceId)
                                                        .setContent(content);
                                                connectionFactory.getConnection().action(context, onUpdateRequest);

                                                activityLogger.log(context, onUpdateRequest, "sync-update",
                                                        onUpdateRequest.getResourcePath(), deltaObject, deltaObject,
                                                        Status.SUCCESS);
                                                break;
                                            }
                                            case DELETE:
                                                // TODO Pass along the old deltaObject - do we have it?
                                                content.put("oldValue", null);
                                                // TODO import SynchronizationService.Action.notifyDelete and ACTION_PARAM_ constants
                                                ActionRequest onDeleteRequest = Requests
                                                        .newActionRequest("sync", "notifyDelete")
                                                        .setAdditionalParameter("resourceContainer",
                                                                resourceContainer)
                                                        .setAdditionalParameter("resourceId", resourceId)
                                                        .setContent(content);
                                                connectionFactory.getConnection().action(context, onDeleteRequest);

                                                activityLogger.log(context, onDeleteRequest, "sync-delete",
                                                        onDeleteRequest.getResourcePath(), null, null,
                                                        Status.SUCCESS);
                                                break;
                                            }
                                        } catch (Exception e) {
                                            failedRecord[0] = SerializerUtil.serializeXmlObject(syncDelta, true);
                                            logger.debug("Failed to synchronize {} object, handle failure using {}",
                                                    syncDelta.getUid(), syncFailureHandler, e);
                                            Map<String, Object> syncFailureMap = new HashMap<String, Object>(6);
                                            syncFailureMap.put("token", syncDelta.getToken().getValue());
                                            syncFailureMap.put("systemIdentifier", systemIdentifier.getName());
                                            syncFailureMap.put("objectType", objectType);
                                            syncFailureMap.put("uid", syncDelta.getUid().getUidValue());
                                            syncFailureMap.put("failedRecord", failedRecord[0]);
                                            try {
                                                syncFailureHandler.invoke(context, syncFailureMap, e);
                                            } catch (SyncHandlerException syncHandlerException) {
                                                // Current contract of the failure handler is that throwing this exception indicates 
                                                // that it should retry for this entry
                                                syncRetry.setValue(true);
                                                syncRetry.setThrowable(syncHandlerException);
                                                logger.debug(
                                                        "Sync failure handler indicated to stop current change set processing until retry handling: {}",
                                                        syncHandlerException.getMessage(), syncHandlerException);
                                            }
                                        }

                                        if (syncRetry.getValue()) {
                                            // Stop the processing of this result set. Next retry will start again after last token.
                                            return false;
                                        } else {
                                            // success (either by original sync or by failure handler)
                                            // Continue the processing of the rest of the result set
                                            lastToken[0] = syncDelta.getToken();
                                            return true;
                                        }
                                    }
                                }, operationOptionsBuilder.build());
                        if (syncRetry.getValue()) {
                            Throwable throwable = syncRetry.getThrowable();
                            Map<String, Object> lastException = new LinkedHashMap<String, Object>(2);
                            lastException.put("throwable", throwable.getMessage());
                            if (null != failedRecord[0]) {
                                lastException.put("syncDelta", failedRecord[0]);
                            }
                            stage.put("lastException", lastException);
                            logger.debug("Live synchronization of {} failed on {}",
                                    new Object[] { objectType, systemIdentifier.getName() }, throwable);
                        } else {
                            if (syncToken != null) {
                                lastToken[0] = syncToken;
                            }
                        }
                    } finally {
                        token = lastToken[0];
                        logger.debug("Synchronization is finished. New LatestSyncToken value: {}", token);
                    }
                }
                if (null != token) {
                    stage.put("connectorData", ConnectorUtil.convertFromSyncToken(token));
                }
            }
        } catch (ResourceException e) {
            logger.debug("Failed to get OperationHelper", e);
            throw new RuntimeException(e);
        } catch (UnsupportedOperationException e) {
            logger.debug("Failed to get OperationOptionsBuilder", e);
            throw new NotFoundException("Failed to get latest sync token", e)
                    .setDetail(new JsonValue(e.getMessage()));
        } catch (Exception e) {
            logger.debug("Failed to get OperationOptionsBuilder", e);
            throw new InternalServerErrorException("Failed to get OperationOptionsBuilder: " + e.getMessage(), e);
        }
        return stage;
    }

    /**
     * Package level setter to allow unit tests to set the logger.
     * @param activityLogger
     */
    void setActivityLogger(ActivityLogger activityLogger) {
        this.activityLogger = activityLogger;
    }
}