com.evolveum.icf.dummy.resource.DummyResource.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.icf.dummy.resource.DummyResource.java

Source

/*
 * Copyright (c) 2010-2015 Evolveum
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.evolveum.icf.dummy.resource;

import java.io.FileNotFoundException;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import com.evolveum.midpoint.util.exception.SystemException;

import org.apache.commons.lang.StringUtils;

import com.evolveum.midpoint.util.DebugDumpable;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;

/**
 * Resource for use with dummy ICF connector.
 * 
 * This is a simple Java object that pretends to be a resource. It has accounts and
 * account schema. It has operations to manipulate accounts, execute scripts and so on
 * almost like a real resource. The purpose is to simulate a real resource with a very 
 * little overhead.
 * 
 * The resource is a singleton, therefore the resource instance can be shared by
 * the connector and the test code. The usual story is like this:
 * 
 * 1) test class fetches first instance of the resource (getInstance). This will cause
 * loading of the resource class in the test (parent) classloader.
 * 
 * 2) test class configures the connector (e.g. schema) usually by calling the populateWithDefaultSchema() method.
 * 
 * 3) test class initializes IDM. This will cause connector initialization. The connector will fetch
 * the instance of dummy resource. As it was loaded by the parent classloader, it will get the same instance
 * as the test class.
 * 
 * 4) test class invokes IDM operation. That will invoke connector and change the resource.
 * 
 * 5) test class will access resource directly to see if the operation went OK.
 * 
 * The dummy resource is a separate package (JAR) from the dummy connector. Connector has its own
 * classloader. If the resource would be the same package as connector, it will get loaded by the
 * connector classloader regardless whether it is already loaded by the parent classloader.
 * 
 * @author Radovan Semancik
 *
 */
public class DummyResource implements DebugDumpable {

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

    private String instanceName;
    private Map<String, DummyObject> allObjects;
    private Map<String, DummyAccount> accounts;
    private Map<String, DummyGroup> groups;
    private Map<String, DummyPrivilege> privileges;
    private List<ScriptHistoryEntry> scriptHistory;
    private DummyObjectClass accountObjectClass;
    private DummyObjectClass groupObjectClass;
    private DummyObjectClass privilegeObjectClass;
    private DummySyncStyle syncStyle;
    private List<DummyDelta> deltas;
    private int latestSyncToken;
    private boolean tolerateDuplicateValues = false;
    private boolean generateDefaultValues = false;
    private boolean enforceUniqueName = true;
    private boolean enforceSchema = true;
    private boolean caseIgnoreId = false;
    private boolean caseIgnoreValues = false;
    private int connectionCount = 0;
    private int groupMembersReadCount = 0;
    private Collection<String> forbiddenNames;

    private BreakMode schemaBreakMode = BreakMode.NONE;
    private BreakMode getBreakMode = BreakMode.NONE;
    private BreakMode addBreakMode = BreakMode.NONE;
    private BreakMode modifyBreakMode = BreakMode.NONE;
    private BreakMode deleteBreakMode = BreakMode.NONE;

    private boolean generateAccountDescriptionOnCreate = false; // simulates volatile behavior (on create)
    private boolean generateAccountDescriptionOnUpdate = false; // simulates volatile behavior (on update)

    // Following two properties are just copied from the connector
    // configuration and can be checked later. They are otherwise
    // completely useless.
    private String uselessString;
    private String uselessGuardedString;

    private static Map<String, DummyResource> instances = new HashMap<String, DummyResource>();

    DummyResource() {
        allObjects = Collections.synchronizedMap(new LinkedHashMap<String, DummyObject>());
        accounts = Collections.synchronizedMap(new LinkedHashMap<String, DummyAccount>());
        groups = Collections.synchronizedMap(new LinkedHashMap<String, DummyGroup>());
        privileges = Collections.synchronizedMap(new LinkedHashMap<String, DummyPrivilege>());
        scriptHistory = new ArrayList<ScriptHistoryEntry>();
        accountObjectClass = new DummyObjectClass();
        groupObjectClass = new DummyObjectClass();
        privilegeObjectClass = new DummyObjectClass();
        syncStyle = DummySyncStyle.NONE;
        deltas = new ArrayList<DummyDelta>();
        latestSyncToken = 0;
    }

    /**
     * Clears everything, just like the resouce was just created.
     */
    public void reset() {
        allObjects.clear();
        accounts.clear();
        groups.clear();
        privileges.clear();
        scriptHistory.clear();
        accountObjectClass = new DummyObjectClass();
        groupObjectClass = new DummyObjectClass();
        privilegeObjectClass = new DummyObjectClass();
        syncStyle = DummySyncStyle.NONE;
        deltas.clear();
        latestSyncToken = 0;
        resetBreakMode();
    }

    public static DummyResource getInstance() {
        return getInstance(null);
    }

    public static DummyResource getInstance(String instanceName) {
        DummyResource instance = instances.get(instanceName);
        if (instance == null) {
            instance = new DummyResource();
            instance.setInstanceName(instanceName);
            instances.put(instanceName, instance);
        }
        return instance;
    }

    public String getInstanceName() {
        return instanceName;
    }

    public void setInstanceName(String instanceName) {
        this.instanceName = instanceName;
    }

    public boolean isTolerateDuplicateValues() {
        return tolerateDuplicateValues;
    }

    public void setTolerateDuplicateValues(boolean tolerateDuplicateValues) {
        this.tolerateDuplicateValues = tolerateDuplicateValues;
    }

    public boolean isGenerateDefaultValues() {
        return generateDefaultValues;
    }

    public void setGenerateDefaultValues(boolean generateDefaultValues) {
        this.generateDefaultValues = generateDefaultValues;
    }

    public boolean isEnforceUniqueName() {
        return enforceUniqueName;
    }

    public void setEnforceUniqueName(boolean enforceUniqueName) {
        this.enforceUniqueName = enforceUniqueName;
    }

    public boolean isEnforceSchema() {
        return enforceSchema;
    }

    public void setEnforceSchema(boolean enforceSchema) {
        this.enforceSchema = enforceSchema;
    }

    public BreakMode getSchemaBreakMode() {
        return schemaBreakMode;
    }

    public void setSchemaBreakMode(BreakMode schemaBreakMode) {
        this.schemaBreakMode = schemaBreakMode;
    }

    public BreakMode getAddBreakMode() {
        return addBreakMode;
    }

    public void setAddBreakMode(BreakMode addBreakMode) {
        this.addBreakMode = addBreakMode;
    }

    public BreakMode getGetBreakMode() {
        return getBreakMode;
    }

    public void setGetBreakMode(BreakMode getBreakMode) {
        this.getBreakMode = getBreakMode;
    }

    public BreakMode getModifyBreakMode() {
        return modifyBreakMode;
    }

    public void setModifyBreakMode(BreakMode modifyBreakMode) {
        this.modifyBreakMode = modifyBreakMode;
    }

    public BreakMode getDeleteBreakMode() {
        return deleteBreakMode;
    }

    public void setDeleteBreakMode(BreakMode deleteBreakMode) {
        this.deleteBreakMode = deleteBreakMode;
    }

    public void setBreakMode(BreakMode breakMode) {
        this.schemaBreakMode = breakMode;
        this.addBreakMode = breakMode;
        this.getBreakMode = breakMode;
        this.modifyBreakMode = breakMode;
        this.deleteBreakMode = breakMode;
    }

    public void resetBreakMode() {
        setBreakMode(BreakMode.NONE);
    }

    public String getUselessString() {
        return uselessString;
    }

    public void setUselessString(String uselessString) {
        this.uselessString = uselessString;
    }

    public String getUselessGuardedString() {
        return uselessGuardedString;
    }

    public void setUselessGuardedString(String uselessGuardedString) {
        this.uselessGuardedString = uselessGuardedString;
    }

    public boolean isCaseIgnoreId() {
        return caseIgnoreId;
    }

    public void setCaseIgnoreId(boolean caseIgnoreId) {
        this.caseIgnoreId = caseIgnoreId;
    }

    public boolean isCaseIgnoreValues() {
        return caseIgnoreValues;
    }

    public void setCaseIgnoreValues(boolean caseIgnoreValues) {
        this.caseIgnoreValues = caseIgnoreValues;
    }

    public boolean isGenerateAccountDescriptionOnCreate() {
        return generateAccountDescriptionOnCreate;
    }

    public void setGenerateAccountDescriptionOnCreate(boolean generateAccountDescriptionOnCreate) {
        this.generateAccountDescriptionOnCreate = generateAccountDescriptionOnCreate;
    }

    public boolean isGenerateAccountDescriptionOnUpdate() {
        return generateAccountDescriptionOnUpdate;
    }

    public void setGenerateAccountDescriptionOnUpdate(boolean generateAccountDescriptionOnUpdate) {
        this.generateAccountDescriptionOnUpdate = generateAccountDescriptionOnUpdate;
    }

    public Collection<String> getForbiddenNames() {
        return forbiddenNames;
    }

    public void setForbiddenNames(Collection<String> forbiddenNames) {
        this.forbiddenNames = forbiddenNames;
    }

    public int getConnectionCount() {
        return connectionCount;
    }

    public synchronized void connect() {
        connectionCount++;
    }

    public synchronized void disconnect() {
        connectionCount--;
    }

    public void assertNoConnections() {
        assert connectionCount == 0 : "Dummy resource: " + connectionCount + " connections still open";
    }

    public int getGroupMembersReadCount() {
        return groupMembersReadCount;
    }

    public void setGroupMembersReadCount(int groupMembersReadCount) {
        this.groupMembersReadCount = groupMembersReadCount;
    }

    public void recordGroupMembersReadCount() {
        groupMembersReadCount++;
        traceOperation("groupMembersRead", groupMembersReadCount);
    }

    public DummyObjectClass getAccountObjectClass() throws ConnectException, FileNotFoundException {
        if (schemaBreakMode == BreakMode.NONE) {
            return accountObjectClass;
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("The schema is not available (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("The schema file not found (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error fetching schema (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error fetching schema (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Schema is not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }

    }

    public DummyObjectClass getGroupObjectClass() {
        return groupObjectClass;
    }

    public DummyObjectClass getPrivilegeObjectClass() {
        return privilegeObjectClass;
    }

    public Collection<DummyAccount> listAccounts() throws ConnectException, FileNotFoundException {
        if (getBreakMode == BreakMode.NONE) {
            return accounts.values();
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }
    }

    private <T extends DummyObject> T getObjectByName(Map<String, T> map, String name)
            throws ConnectException, FileNotFoundException {
        if (!enforceUniqueName) {
            throw new IllegalStateException(
                    "Attempt to search object by name while resource is in non-unique name mode");
        }
        if (getBreakMode == BreakMode.NONE) {
            return map.get(normalize(name));
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }
    }

    public DummyAccount getAccountByUsername(String username) throws ConnectException, FileNotFoundException {
        return getObjectByName(accounts, username);
    }

    public DummyGroup getGroupByName(String name) throws ConnectException, FileNotFoundException {
        return getObjectByName(groups, name);
    }

    public DummyPrivilege getPrivilegeByName(String name) throws ConnectException, FileNotFoundException {
        return getObjectByName(privileges, name);
    }

    private <T extends DummyObject> T getObjectById(Class<T> expectedClass, String id)
            throws ConnectException, FileNotFoundException {
        if (getBreakMode == BreakMode.NONE) {
            DummyObject dummyObject = allObjects.get(id);
            if (dummyObject == null) {
                return null;
            }
            if (!expectedClass.isInstance(dummyObject)) {
                throw new IllegalStateException("Arrrr! Wanted " + expectedClass + " with ID " + id + " but got "
                        + dummyObject + " instead");
            }
            return (T) dummyObject;
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }
    }

    public DummyAccount getAccountById(String id) throws ConnectException, FileNotFoundException {
        return getObjectById(DummyAccount.class, id);
    }

    public DummyGroup getGroupById(String id) throws ConnectException, FileNotFoundException {
        return getObjectById(DummyGroup.class, id);
    }

    public DummyPrivilege getPrivilegeById(String id) throws ConnectException, FileNotFoundException {
        return getObjectById(DummyPrivilege.class, id);
    }

    public Collection<DummyGroup> listGroups() throws ConnectException, FileNotFoundException {
        if (getBreakMode == BreakMode.NONE) {
            return groups.values();
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }
    }

    public Collection<DummyPrivilege> listPrivileges() throws ConnectException, FileNotFoundException {
        if (getBreakMode == BreakMode.NONE) {
            return privileges.values();
        } else if (schemaBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (schemaBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (schemaBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (schemaBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }
    }

    private synchronized <T extends DummyObject> String addObject(Map<String, T> map, T newObject)
            throws ObjectAlreadyExistsException, ConnectException, FileNotFoundException, SchemaViolationException {
        if (addBreakMode == BreakMode.NONE) {
            // just go on
        } else if (addBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error during add (simulated error)");
        } else if (addBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error during add (simulated error)");
        } else if (addBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error during add (simulated error)");
        } else if (addBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic rutime error during add (simulated error)");
        } else if (addBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Unsupported operation: add (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown break mode " + addBreakMode);
        }

        Class<? extends DummyObject> type = newObject.getClass();
        String normalName = normalize(newObject.getName());
        if (normalName != null && forbiddenNames != null && forbiddenNames.contains(normalName)) {
            throw new ObjectAlreadyExistsException(normalName + " is forbidden to use as an object name");
        }

        String newId = UUID.randomUUID().toString();
        newObject.setId(newId);
        if (allObjects.containsKey(newId)) {
            throw new IllegalStateException("The hell is frozen over. The impossible has happened. ID " + newId
                    + " already exists (" + type.getSimpleName() + " with identifier " + normalName + ")");
        }

        //this is "resource-generated" attribute (used to simulate resource which generate by default attributes which we need to sync)
        if (generateDefaultValues) {
            //         int internalId = allObjects.size();
            newObject.addAttributeValue(DummyAccount.ATTR_INTERNAL_ID, new Random().nextInt());
        }

        String mapKey;
        if (enforceUniqueName) {
            mapKey = normalName;
        } else {
            mapKey = newId;
        }

        if (map.containsKey(mapKey)) {
            throw new ObjectAlreadyExistsException(
                    type.getSimpleName() + " with name '" + normalName + "' already exists");
        }

        newObject.setResource(this);
        map.put(mapKey, newObject);
        allObjects.put(newId, newObject);

        if (syncStyle != DummySyncStyle.NONE) {
            int syncToken = nextSyncToken();
            DummyDelta delta = new DummyDelta(syncToken, type, newId, newObject.getName(), DummyDeltaType.ADD);
            deltas.add(delta);
        }

        return newObject.getName();
    }

    private synchronized <T extends DummyObject> void deleteObjectByName(Class<T> type, Map<String, T> map,
            String name) throws ObjectDoesNotExistException, ConnectException, FileNotFoundException {
        if (deleteBreakMode == BreakMode.NONE) {
            // go on
        } else if (deleteBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (deleteBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (deleteBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (deleteBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (deleteBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }

        String normalName = normalize(name);
        T existingObject;

        if (!enforceUniqueName) {
            throw new IllegalStateException("Whoops! got into deleteObjectByName without enforceUniqueName");
        }

        if (map.containsKey(normalName)) {
            existingObject = map.get(normalName);
            map.remove(normalName);
            allObjects.remove(existingObject.getId());
        } else {
            throw new ObjectDoesNotExistException(
                    type.getSimpleName() + " with name '" + normalName + "' does not exist");
        }

        if (syncStyle != DummySyncStyle.NONE) {
            int syncToken = nextSyncToken();
            DummyDelta delta = new DummyDelta(syncToken, type, existingObject.getId(), name, DummyDeltaType.DELETE);
            deltas.add(delta);
        }
    }

    public void deleteAccountById(String id)
            throws ConnectException, FileNotFoundException, ObjectDoesNotExistException {
        deleteObjectById(DummyAccount.class, accounts, id);
    }

    public void deleteGroupById(String id)
            throws ConnectException, FileNotFoundException, ObjectDoesNotExistException {
        deleteObjectById(DummyGroup.class, groups, id);
    }

    public void deletePrivilegeById(String id)
            throws ConnectException, FileNotFoundException, ObjectDoesNotExistException {
        deleteObjectById(DummyPrivilege.class, privileges, id);
    }

    private synchronized <T extends DummyObject> void deleteObjectById(Class<T> type, Map<String, T> map, String id)
            throws ObjectDoesNotExistException, ConnectException, FileNotFoundException {
        if (deleteBreakMode == BreakMode.NONE) {
            // go on
        } else if (deleteBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (deleteBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (deleteBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (deleteBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (deleteBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }

        DummyObject object = allObjects.get(id);
        if (object == null) {
            throw new ObjectDoesNotExistException(type.getSimpleName() + " with id '" + id + "' does not exist");
        }
        if (!type.isInstance(object)) {
            throw new IllegalStateException(
                    "Arrrr! Wanted " + type + " with ID " + id + " but got " + object + " instead");
        }
        T existingObject = (T) object;
        String normalName = normalize(object.getName());

        allObjects.remove(id);

        String mapKey;
        if (enforceUniqueName) {
            mapKey = normalName;
        } else {
            mapKey = id;
        }

        if (map.containsKey(mapKey)) {
            map.remove(mapKey);
        } else {
            throw new ObjectDoesNotExistException(
                    type.getSimpleName() + " with name '" + normalName + "' does not exist");
        }

        if (syncStyle != DummySyncStyle.NONE) {
            int syncToken = nextSyncToken();
            DummyDelta delta = new DummyDelta(syncToken, type, id, object.getName(), DummyDeltaType.DELETE);
            deltas.add(delta);
        }
    }

    private <T extends DummyObject> void renameObject(Class<T> type, Map<String, T> map, String id, String oldName,
            String newName) throws ObjectDoesNotExistException, ObjectAlreadyExistsException, ConnectException,
            FileNotFoundException {
        if (modifyBreakMode == BreakMode.NONE) {
            // go on
        } else if (modifyBreakMode == BreakMode.NETWORK) {
            throw new ConnectException("Network error (simulated error)");
        } else if (modifyBreakMode == BreakMode.IO) {
            throw new FileNotFoundException("IO error (simulated error)");
        } else if (modifyBreakMode == BreakMode.GENERIC) {
            // The connector will react with generic exception
            throw new IllegalArgumentException("Generic error (simulated error)");
        } else if (modifyBreakMode == BreakMode.RUNTIME) {
            // The connector will just pass this up
            throw new IllegalStateException("Generic error (simulated error)");
        } else if (modifyBreakMode == BreakMode.UNSUPPORTED) {
            throw new UnsupportedOperationException("Not supported (simulated error)");
        } else {
            // This is a real error. Use this strange thing to make sure it passes up
            throw new RuntimeException("Unknown schema break mode " + schemaBreakMode);
        }

        T existingObject;
        if (enforceUniqueName) {
            String normalOldName = normalize(oldName);
            String normalNewName = normalize(newName);
            existingObject = map.get(normalOldName);
            if (existingObject == null) {
                throw new ObjectDoesNotExistException("Cannot rename, " + type.getSimpleName() + " with username '"
                        + normalOldName + "' does not exist");
            }
            if (map.containsKey(normalNewName)) {
                throw new ObjectAlreadyExistsException("Cannot rename, " + type.getSimpleName() + " with username '"
                        + normalNewName + "' already exists");
            }
            map.put(normalNewName, existingObject);
            map.remove(normalOldName);
        } else {
            existingObject = (T) allObjects.get(id);
        }
        existingObject.setName(newName);
        if (existingObject instanceof DummyAccount) {
            changeDescriptionIfNeeded((DummyAccount) existingObject);
        }
    }

    public String addAccount(DummyAccount newAccount)
            throws ObjectAlreadyExistsException, ConnectException, FileNotFoundException, SchemaViolationException {
        if (generateAccountDescriptionOnCreate
                && newAccount.getAttributeValue(DummyAccount.ATTR_DESCRIPTION_NAME) == null) {
            newAccount.addAttributeValue(DummyAccount.ATTR_DESCRIPTION_NAME,
                    "Description of " + newAccount.getName());
        }
        return addObject(accounts, newAccount);
    }

    public void deleteAccountByName(String id)
            throws ObjectDoesNotExistException, ConnectException, FileNotFoundException {
        deleteObjectByName(DummyAccount.class, accounts, id);
    }

    public void renameAccount(String id, String oldUsername, String newUsername) throws ObjectDoesNotExistException,
            ObjectAlreadyExistsException, ConnectException, FileNotFoundException, SchemaViolationException {
        renameObject(DummyAccount.class, accounts, id, oldUsername, newUsername);
        for (DummyGroup group : groups.values()) {
            if (group.containsMember(oldUsername)) {
                group.removeMember(oldUsername);
                group.addMember(newUsername);
            }
        }
    }

    public void changeDescriptionIfNeeded(DummyAccount account) {
        if (generateAccountDescriptionOnCreate) {
            try {
                account.replaceAttributeValue(DummyAccount.ATTR_DESCRIPTION_NAME,
                        "Updated description of " + account.getName());
            } catch (SchemaViolationException | ConnectException | FileNotFoundException e) {
                throw new SystemException("Couldn't replace the 'description' attribute value", e);
            }
        }
    }

    public String addGroup(DummyGroup newGroup)
            throws ObjectAlreadyExistsException, ConnectException, FileNotFoundException, SchemaViolationException {
        return addObject(groups, newGroup);
    }

    public void deleteGroupByName(String id)
            throws ObjectDoesNotExistException, ConnectException, FileNotFoundException {
        deleteObjectByName(DummyGroup.class, groups, id);
    }

    public void renameGroup(String id, String oldName, String newName) throws ObjectDoesNotExistException,
            ObjectAlreadyExistsException, ConnectException, FileNotFoundException {
        renameObject(DummyGroup.class, groups, id, oldName, newName);
    }

    public String addPrivilege(DummyPrivilege newGroup)
            throws ObjectAlreadyExistsException, ConnectException, FileNotFoundException, SchemaViolationException {
        return addObject(privileges, newGroup);
    }

    public void deletePrivilegeByName(String id)
            throws ObjectDoesNotExistException, ConnectException, FileNotFoundException {
        deleteObjectByName(DummyPrivilege.class, privileges, id);
    }

    public void renamePrivilege(String id, String oldName, String newName) throws ObjectDoesNotExistException,
            ObjectAlreadyExistsException, ConnectException, FileNotFoundException {
        renameObject(DummyPrivilege.class, privileges, id, oldName, newName);
    }

    void recordModify(DummyObject dObject) {
        if (syncStyle != DummySyncStyle.NONE) {
            int syncToken = nextSyncToken();
            DummyDelta delta = new DummyDelta(syncToken, dObject.getClass(), dObject.getId(), dObject.getName(),
                    DummyDeltaType.MODIFY);
            deltas.add(delta);
        }
    }

    /**
     * Returns script history ordered chronologically (oldest first).
     * @return script history
     */
    public List<ScriptHistoryEntry> getScriptHistory() {
        return scriptHistory;
    }

    /**
     * Clears the script history.
     */
    public void purgeScriptHistory() {
        scriptHistory.clear();
    }

    /**
     * Pretend to run script on the resource.
     * The script is actually not executed, it is only recorded in the script history
     * and can be fetched by getScriptHistory().
     * 
     * @param scriptCode code of the script
     */
    public void runScript(String language, String scriptCode, Map<String, Object> params) {
        scriptHistory.add(new ScriptHistoryEntry(language, scriptCode, params));
    }

    /**
     * Populates the resource with some kind of "default" schema. This is a schema that should suit
     * majority of basic test cases.
     */
    public void populateWithDefaultSchema() {
        accountObjectClass.clear();
        accountObjectClass.addAttributeDefinition(DummyAccount.ATTR_FULLNAME_NAME, String.class, true, false);
        accountObjectClass.addAttributeDefinition(DummyAccount.ATTR_INTERNAL_ID, String.class, false, false);
        accountObjectClass.addAttributeDefinition(DummyAccount.ATTR_DESCRIPTION_NAME, String.class, false, false);
        accountObjectClass.addAttributeDefinition(DummyAccount.ATTR_INTERESTS_NAME, String.class, false, true);
        accountObjectClass.addAttributeDefinition(DummyAccount.ATTR_PRIVILEGES_NAME, String.class, false, true);
        groupObjectClass.clear();
        groupObjectClass.addAttributeDefinition(DummyGroup.ATTR_MEMBERS_NAME, String.class, false, true);
        privilegeObjectClass.clear();
    }

    public DummySyncStyle getSyncStyle() {
        return syncStyle;
    }

    public void setSyncStyle(DummySyncStyle syncStyle) {
        this.syncStyle = syncStyle;
    }

    private synchronized int nextSyncToken() {
        return ++latestSyncToken;
    }

    public int getLatestSyncToken() {
        return latestSyncToken;
    }

    private String normalize(String id) {
        if (caseIgnoreId) {
            return StringUtils.lowerCase(id);
        } else {
            return id;
        }
    }

    public List<DummyDelta> getDeltasSince(int syncToken) {
        List<DummyDelta> result = new ArrayList<DummyDelta>();
        for (DummyDelta delta : deltas) {
            if (delta.getSyncToken() > syncToken) {
                result.add(delta);
            }
        }
        return result;
    }

    private void traceOperation(String opName, long counter) {
        LOGGER.info("MONITOR dummy '{}' {} ({})", instanceName, opName, counter);
        if (LOGGER.isDebugEnabled()) {
            StackTraceElement[] fullStack = Thread.currentThread().getStackTrace();
            String immediateClass = null;
            String immediateMethod = null;
            StringBuilder sb = new StringBuilder();
            for (StackTraceElement stackElement : fullStack) {
                if (stackElement.getClassName().equals(DummyResource.class.getName())
                        || stackElement.getClassName().equals(Thread.class.getName())) {
                    // skip our own calls
                    continue;
                }
                if (immediateClass == null) {
                    immediateClass = stackElement.getClassName();
                    immediateMethod = stackElement.getMethodName();
                }
                sb.append(stackElement.toString());
                sb.append("\n");
            }
            LOGGER.debug("MONITOR dummy '{}' {} ({}): {} {}",
                    new Object[] { instanceName, opName, counter, immediateClass, immediateMethod });
            LOGGER.trace("MONITOR dummy '{}' {} ({}):\n{}", new Object[] { instanceName, opName, counter, sb });
        }
    }

    @Override
    public String debugDump() {
        return debugDump(0);
    }

    @Override
    public String debugDump(int indent) {
        StringBuilder sb = new StringBuilder(toString());
        DebugUtil.indentDebugDump(sb, indent);
        sb.append("\nAccounts:");
        for (Entry<String, DummyAccount> entry : accounts.entrySet()) {
            sb.append("\n  ");
            sb.append(entry.getKey());
            sb.append(": ");
            sb.append(entry.getValue());
        }
        sb.append("\nGroups:");
        for (Entry<String, DummyGroup> entry : groups.entrySet()) {
            sb.append("\n  ");
            sb.append(entry.getKey());
            sb.append(": ");
            sb.append(entry.getValue());
        }
        sb.append("\nDeltas:");
        for (DummyDelta delta : deltas) {
            sb.append("\n  ");
            sb.append(delta);
        }
        sb.append("\nLatest token:").append(latestSyncToken);
        return sb.toString();
    }

    @Override
    public String toString() {
        return "DummyResource(" + accounts.size() + " accounts, " + groups.size() + " groups)";
    }

}