All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cern.entwined.TransactionalMap Maven / Gradle / Ivy

/*
 * Entwined STM
 * 
 * (c) Copyright 2013 CERN. This software is distributed under the terms of the Apache License Version 2.0, copied
 * verbatim in the file "COPYING". In applying this licence, CERN does not waive the privileges and immunities granted
 * to it by virtue of its status as an Intergovernmental Organization or submit itself to any jurisdiction.
 */
package cern.entwined;

import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;

import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;

import cern.entwined.exception.ConflictException;

import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;

/**
 * Implementation of a transactional map. It logs all the reads and modifications of the data, and uses it at commit
 * time to detect conflicting transactions.
 * 
 * @param  The map key type.
 * @param  The map value type.
 * @author Ivan Koblik
 */
public class TransactionalMap extends SemiPersistent> implements OpaqueMap {

    /**
     * The unmodifiable backbone of {@link TransactionalMap}.
     */
    private final Map sourceMap;

    /**
     * Map of key value pairs of all locally added or modified values.
     */
    private final Map pendingModifications = new HashMap();

    /**
     * Set of keys of all the items locally removed from the map.
     */
    private final Set pendingDeletions = new HashSet();

    /**
     * Set of keys of all the items that were accessed or attempted to be accessed (i.e. for non-existent keys).
     */
    private final Set accessed = new HashSet();

    /**
     * This flag is used to mark the "entire world" as has been accessed. The reasoning is quite simple; if a user knows
     * all the entries in the map he also knows all entries that aren't there, which may not be true in the context of
     * the global map. Due to this {@link TransactionalMap} disallows commit if global state changes in any way even if
     * it is just a new entry.
     */
    private boolean globallyAccessed = false;

    /**
     * This flag is set to true the first time clear method is called.
     */
    private boolean cleared = false;

    /**
     * Constructs a new empty {@link TransactionalMap}.
     */
    @SuppressWarnings("unchecked")
    public TransactionalMap() {
        this(Collections.EMPTY_MAP, false);
    }

    /**
     * Constructs new {@link TransactionalMap} initializing it with the given collection. Passed collection is copied.
     * 
     * @param sourceMap The {@link TransactionalMap} initial state.
     */
    public TransactionalMap(Map sourceMap) {
        this(sourceMap, true);
    }

    /**
     * Constructs new {@link TransactionalMap} initializing it with the given collection.
     * 
     * @param sourceMap The {@link TransactionalMap} initial state.
     * @param cloneSource If true passed collection is copied.
     */
    private TransactionalMap(Map sourceMap, boolean cloneSource) {
        Utils.checkNull("Source map", sourceMap);
        if (cloneSource) {
            this.sourceMap = Collections.unmodifiableMap(new HashMap(sourceMap));
        } else {
            this.sourceMap = sourceMap;
        }
    }

    @Override
    public int size() {
        this.markGloballyAccessed();
        Set keys = Sets.union(this.sourceMap.keySet(), this.pendingModifications.keySet());
        return keys.size() - this.pendingDeletions.size();
    }

    @Override
    public boolean isEmpty() {
        boolean empty = (this.sourceMap.size() == this.pendingDeletions.size()) && this.pendingModifications.isEmpty();
        if (empty) {
            this.markGloballyAccessed();
        }
        return empty;
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#clear()
     */
    @Override
    public void clear() {
        this.markCleared();
        this.pendingDeletions.addAll(sourceMap.keySet());
        this.pendingModifications.clear();
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#containsKey(java.lang.Object)
     */
    @Override
    public boolean containsKey(K key) {
        this.markAccessed(key);
        return (sourceMap.containsKey(key) || this.pendingModifications.containsKey(key))
                && !pendingDeletions.contains(key);
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#get(java.lang.Object)
     */
    @Override
    public V get(K key) {
        this.markAccessed(key);
        if (this.pendingDeletions.contains(key)) {
            return null;
        } else if (this.pendingModifications.containsKey(key)) {
            return this.pendingModifications.get(key);
        }
        return this.sourceMap.get(key);
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#put(java.lang.Object, java.lang.Object)
     */
    @Override
    public V put(K key, V value) {
        this.accessed.add(key);
        this.pendingDeletions.remove(key);
        V oldValue = this.pendingModifications.put(key, value);
        return null != oldValue ? oldValue : this.sourceMap.get(key);
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#putAll(java.util.Map)
     */
    @Override
    public void putAll(Map m) {
        Utils.checkNull("Map", m);
        this.accessed.addAll(m.keySet());
        for (Entry entry : m.entrySet()) {
            this.put(entry.getKey(), entry.getValue());
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.OpaqueMap#remove(java.lang.Object)
     */
    @Override
    public V remove(K key) {
        V oldValue = this.get(key); // Getting old value and marking it as accessed
        if (this.sourceMap.containsKey(key)) {
            this.pendingDeletions.add(key);
        }
        this.pendingModifications.remove(key);
        return oldValue;
    }

    //
    // Views
    //

    @Override
    public Set keySet() {
        return new KeySet();
    }

    //
    // Transactional methods
    //

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.Transactional#cleanCopy()
     */
    @Override
    public TransactionalMap cleanCopy() {
        return new TransactionalMap(this.sourceMap, false);
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.SemiPersistent#dirtyCopy()
     */
    @Override
    protected TransactionalMap dirtyCopy() {
        TransactionalMap copy = new TransactionalMap(this.sourceMap, false);
        copy.globallyAccessed = this.globallyAccessed;
        copy.markAccessed(this.accessed);
        copy.pendingDeletions.addAll(this.pendingDeletions);
        copy.pendingModifications.putAll(this.pendingModifications);
        return copy;
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.SemiPersistent#update(java.lang.Object, boolean)
     */
    @Override
    protected void update(TransactionalMap changes, boolean onlyReadLogs) {
        Utils.checkNull("Local changes", changes);
        if (this.sourceMap != changes.sourceMap) {
            throw new IllegalArgumentException("Updates are only possible for collections with the same source");
        }
        if (changes.globallyAccessed) {
            markGloballyAccessed();
        }
        this.markAccessed(changes.accessed);
        if (!onlyReadLogs) {
            this.pendingModifications.clear();
            this.pendingModifications.putAll(changes.pendingModifications);
            this.pendingDeletions.clear();
            this.pendingDeletions.addAll(changes.pendingDeletions);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.ConflictAware#commit(java.lang.Object)
     */
    @Override
    public TransactionalMap commit(TransactionalMap globalState) {
        Utils.checkNull("Global state", globalState);
        if (!globalState.pendingDeletions.isEmpty() || !globalState.pendingModifications.isEmpty()
                || !globalState.accessed.isEmpty() || globalState.globallyAccessed) {
            throw new IllegalArgumentException("Global state map must be commited before calling this method");
        }

        // Checking for conflicts
        if (this.globallyAccessed) {
            if (!globalState.sourceMap.equals(this.sourceMap)) {
                throw new ConflictException("All the items of this map have been accessed "
                        + "this prohibits commit in the case of concurrent changes");
            }
        }
        for (K key : this.accessed) {
            checkConsistency(globalState.sourceMap, key);
        }

        // Return current global state if there are no local modifications
        if (this.pendingDeletions.isEmpty() && this.pendingModifications.isEmpty()) {
            return globalState;
        }

        // Getting a copy of the global map
        HashMap globalMapCopy = new HashMap(globalState.sourceMap);

        // Apply addition or modification
        for (Entry entry : this.pendingModifications.entrySet()) {
            globalMapCopy.put(entry.getKey(), entry.getValue());
        }

        // Apply deletion
        for (K key : this.pendingDeletions) {
            globalMapCopy.remove(key);
        }

        // Returning a new instance of the map
        return new TransactionalMap(globalMapCopy);
    }

    //
    // Private methods
    //

    /**
     * Marks given key as accessed.
     * 
     * @param key The key to mark as accessed.
     */
    private void markAccessed(K key) {
        if (!this.globallyAccessed) {
            this.accessed.add(key);
        }
    }

    /**
     * Marks given collection of keys as accessed.
     * 
     * @param key The keys to mark as accessed.
     */
    private void markAccessed(Collection keys) {
        if (!this.globallyAccessed) {
            this.accessed.addAll(keys);
        }
    }

    /**
     * Marks the entire space of keys as accessed unless the map has been cleared.
     */
    private void markGloballyAccessed() {
        if (!this.cleared) {
            // Global access is allowed after the map has been cleared.
            this.globallyAccessed = true;
            this.accessed.clear();
        }
    }

    /**
     * Marks the map as cleared and marks all its items as accessed unless it has already been globally accessed.
     */
    private void markCleared() {
        if (!this.globallyAccessed) {
            this.cleared = true;
            this.markAccessed(sourceMap.keySet());
        }
    }

    /**
     * Simply checks if values corresponding to the key are the same in the global and source maps.
     * 
     * @param globalMap The global map.
     * @param key The key corresponding to the value to be checked.
     */
    private void checkConsistency(Map globalMap, K key) {
        V sourceValue = this.sourceMap.get(key);
        V globalValue = globalMap.get(key);
        if ((sourceValue != globalValue) || // 
((null == sourceValue || null == globalValue)//
&& (this.sourceMap.containsKey(key) ^ globalMap.containsKey(key)))) { throw new ConflictException("Conflicting changes for [" + key + "]"); } } /** * Dynamic view on the keys of the map. * * @author Ivan Koblik */ private class KeySet extends AbstractSet { @Override public Iterator iterator() { return new KeyIterator(); } @Override public int size() { return TransactionalMap.this.size(); } @Override public boolean isEmpty() { return TransactionalMap.this.isEmpty(); } @Override @SuppressWarnings("unchecked") public boolean contains(Object o) { return containsKey((K) o); } @Override @SuppressWarnings("unchecked") public boolean remove(Object o) { boolean result = TransactionalMap.this.containsKey((K) o); TransactionalMap.this.remove((K) o); return result; } @Override public boolean removeAll(Collection c) { boolean modified = false; for (Iterator i = c.iterator(); i.hasNext();) { modified |= this.remove(i.next()); } return modified; } @Override public boolean retainAll(Collection c) { throw new UnsupportedOperationException(); } @Override public void clear() { TransactionalMap.this.clear(); } } /** * Iterator over the keys of the map. * * @author Ivan Koblik */ private class KeyIterator implements Iterator { /** * The delegate iterator that is a concatenation of sourceMap and pendingModifications minus pendingDeletions. */ private final Iterator keyIterator; /** * Constructs the iterator by initializing the delegate iterator. */ public KeyIterator() { // Concatenate iterators of sourceMap and pendingModifications. Iterator unfiltered = Iterators.concat(TransactionalMap.this.sourceMap.keySet().iterator(), TransactionalMap.this.pendingModifications.keySet().iterator()); // Remove the elements from pendingDeletions. keyIterator = Iterators.filter(unfiltered, not(in(TransactionalMap.this.pendingDeletions))); } @Override public boolean hasNext() { boolean hasNext = keyIterator.hasNext(); if (!hasNext) { TransactionalMap.this.markGloballyAccessed(); } return hasNext; } @Override public K next() { try { return keyIterator.next(); } catch (NoSuchElementException ex) { TransactionalMap.this.markGloballyAccessed(); throw ex; } } @Override public void remove() { throw new UnsupportedOperationException(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy