cern.entwined.TransactionalMap.java Source code

Java tutorial

Introduction

Here is the source code for cern.entwined.TransactionalMap.java

Source

/*
 * 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 <K> The map key type.
 * @param <V> The map value type.
 * @author Ivan Koblik
 */
public class TransactionalMap<K, V> extends SemiPersistent<TransactionalMap<K, V>> implements OpaqueMap<K, V> {

    /**
     * The unmodifiable backbone of {@link TransactionalMap}.
     */
    private final Map<K, V> sourceMap;

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

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

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

    /**
     * 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<K, V> 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<K, V> sourceMap, boolean cloneSource) {
        Utils.checkNull("Source map", sourceMap);
        if (cloneSource) {
            this.sourceMap = Collections.unmodifiableMap(new HashMap<K, V>(sourceMap));
        } else {
            this.sourceMap = sourceMap;
        }
    }

    @Override
    public int size() {
        this.markGloballyAccessed();
        Set<K> 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<? extends K, ? extends V> m) {
        Utils.checkNull("Map", m);
        this.accessed.addAll(m.keySet());
        for (Entry<? extends K, ? extends V> 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<K> keySet() {
        return new KeySet();
    }

    //
    // Transactional methods
    //

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

    /*
     * (non-Javadoc)
     * 
     * @see cern.oasis.server.stm.SemiPersistent#dirtyCopy()
     */
    @Override
    protected TransactionalMap<K, V> dirtyCopy() {
        TransactionalMap<K, V> copy = new TransactionalMap<K, V>(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<K, V> 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<K, V> commit(TransactionalMap<K, V> 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<K, V> globalMapCopy = new HashMap<K, V>(globalState.sourceMap);

        // Apply addition or modification
        for (Entry<K, V> 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<K, V>(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<K> 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 <b>same</b> 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<K, V> globalMap, K key) {
        V sourceValue = this.sourceMap.get(key);
        V globalValue = globalMap.get(key);
        if ((sourceValue != globalValue) || // <br>
                ((null == sourceValue || null == globalValue)// <br>
                        && (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<K> {
        @Override
        public Iterator<K> 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<K> {
        /**
         * The delegate iterator that is a concatenation of sourceMap and pendingModifications minus pendingDeletions.
         */
        private final Iterator<K> keyIterator;

        /**
         * Constructs the iterator by initializing the delegate iterator.
         */
        public KeyIterator() {
            // Concatenate iterators of sourceMap and pendingModifications.
            Iterator<K> 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();
        }
    }
}