OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.utils;

import org.openconcerto.utils.cc.AbstractMapDecorator;

import java.util.AbstractCollection;
import java.util.Arrays;
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.NoSuchElementException;
import java.util.Set;

/**
 * Allow to map keys to collections. This map always allow <code>null</code> items inside mapped
 * collections, but it may restrict null collections, see {@link Mode}.
 * 
 * @author Sylvain
 * 
 * @param <K> the type of keys maintained by this map
 * @param <C> the type of mapped collections
 * @param <V> the type of elements of the collections
 */
public abstract class CollectionMap2<K, C extends Collection<V>, V> extends AbstractMapDecorator<K, C> implements Cloneable, CollectionMap2Itf<K, C, V> {

    static final int DEFAULT_INITIAL_CAPACITY = 16;

    private static final String toStr(final Object o) {
        return o == null ? "null" : "'" + o + "'";
    }

    public static enum Mode {
        /**
         * Mapped collections cannot be <code>null</code>.
         */
        NULL_FORBIDDEN,
        /**
         * Mapped collections can be <code>null</code>, but some methods may throw
         * {@link NullPointerException}.
         * 
         * @see CollectionMap2#addAll(Object, Collection)
         * @see CollectionMap2#removeAll(Object, Collection)
         */
        NULL_ALLOWED,
        /**
         * Mapped collections can be <code>null</code>, meaning every possible item. Thus no method
         * throws {@link NullPointerException}.
         */
        NULL_MEANS_ALL
    }

    static protected final Mode DEFAULT_MODE = Mode.NULL_FORBIDDEN;
    static private final Boolean DEFAULT_emptyCollSameAsNoColl = null;

    private final boolean emptyCollSameAsNoColl;
    private final Mode mode;
    private transient Collection<V> allValues = null;

    public CollectionMap2() {
        this(DEFAULT_MODE);
    }

    public CollectionMap2(final Mode mode) {
        this(mode, DEFAULT_emptyCollSameAsNoColl);
    }

    public CollectionMap2(final Map<K, C> delegate, final Mode mode) {
        this(delegate, mode, DEFAULT_emptyCollSameAsNoColl);
    }

    public CollectionMap2(final Mode mode, final Boolean emptyCollSameAsNoColl) {
        this(DEFAULT_INITIAL_CAPACITY, mode, emptyCollSameAsNoColl);
    }

    public CollectionMap2(final int initialCapacity) {
        this(initialCapacity, DEFAULT_MODE, DEFAULT_emptyCollSameAsNoColl);
    }

    public CollectionMap2(final int initialCapacity, final Mode mode, final Boolean emptyCollSameAsNoColl) {
        this(new HashMap<K, C>(initialCapacity), mode, emptyCollSameAsNoColl);
    }

    /**
     * Create a new instance with the passed delegate. The delegate is *not* cleared, this allows to
     * decorate an existing Map but it also means that the existing collections might not be the
     * exact same type as those returned by {@link #createCollection(Collection)}.
     * 
     * @param delegate the map to use, it must not be modified afterwards.
     * @param mode how to handle null values.
     * @param emptyCollSameAsNoColl for {@link #getCollection(Object)} : whether the lack of an
     *        entry is the same as an entry with an empty collection, can be <code>null</code>.
     */
    public CollectionMap2(final Map<K, C> delegate, final Mode mode, final Boolean emptyCollSameAsNoColl) {
        super(delegate);
        if (mode == null)
            throw new NullPointerException("Null mode");
        this.mode = mode;
        this.emptyCollSameAsNoColl = emptyCollSameAsNoColl == null ? mode == Mode.NULL_MEANS_ALL : emptyCollSameAsNoColl;
        checkMode();
    }

    private final void checkMode() {
        assert this.mode != null : "Called too early";
        if (this.mode == Mode.NULL_FORBIDDEN && this.containsValue(null))
            throw new IllegalArgumentException("Null collection");
    }

    // ** copy constructors

    // ATTN getDelegate() is not in CollectionMap2Itf, so if one copies an unmodifiableMap() this
    // constructor won't be used and the delegate will be the default HashMap (even if the source
    // used a LinkedHashMap).
    public CollectionMap2(final CollectionMap2<K, C, ? extends V> m) {
        this(CopyUtils.copy(m.getDelegate()), m);
    }

    public CollectionMap2(final Map<? extends K, ? extends Collection<? extends V>> m) {
        this(new HashMap<K, C>(m.size()), m);
    }

    /**
     * Create a new instance with the passed delegate and filling it with the passed map.
     * 
     * @param delegate the map to use, it will be cleared and must not be modified afterwards.
     * @param m the values to put in this, if it's an instance of {@link CollectionMap2} the
     *        {@link #getMode() mode} and {@link #isEmptyCollSameAsNoColl()} will be copied as well.
     */
    public CollectionMap2(final Map<K, C> delegate, final Map<? extends K, ? extends Collection<? extends V>> m) {
        // don't use super(Map) since it doesn't copy the collections
        // also its type is more restrictive
        super(delegate);
        if (m instanceof CollectionMap2) {
            final CollectionMap2<?, ?, ?> collM = (CollectionMap2<?, ?, ?>) m;
            this.mode = collM.getMode();
            this.emptyCollSameAsNoColl = collM.isEmptyCollSameAsNoColl();
        } else {
            this.mode = DEFAULT_MODE;
            this.emptyCollSameAsNoColl = this.mode == Mode.NULL_MEANS_ALL;
        }
        // delegate might not contain the same instances of collections (i.e. LinkedList vs
        // ArrayList)
        this.clear();
        this.putAllCollections(m);
    }

    @Override
    public final Mode getMode() {
        return this.mode;
    }

    @Override
    public final boolean isEmptyCollSameAsNoColl() {
        return this.emptyCollSameAsNoColl;
    }

    public final C getNonNullIfMissing(final Object key) {
        return this.get(key, false, true);
    }

    @Override
    public final C getNonNull(final K key) {
        return this.get(key, false, false);
    }

    private final C getNonNullColl(final C res) {
        return res == null ? this.createCollection(Collections.<V> emptySet()) : res;
    }

    /**
     * Get the collection mapped to the passed key. Note : <code>get(key, true, true)</code> is
     * equivalent to <code>get(key)</code>.
     * 
     * @param key the key whose associated value is to be returned.
     * @param nullIfMissing only relevant if the key isn't contained : if <code>true</code>
     *        <code>null</code> will be returned, otherwise an empty collection.
     * @param nullIfPresent only relevant if the key is mapped to <code>null</code> : if
     *        <code>true</code> <code>null</code> will be returned, otherwise an empty collection.
     * @return the non {@code null} value to which the specified key is mapped, otherwise
     *         {@code null} or empty collection depending on the other parameters.
     */
    @Override
    public final C get(final Object key, final boolean nullIfMissing, final boolean nullIfPresent) {
        if (nullIfMissing == nullIfPresent) {
            final C res = super.get(key);
            if (res != null || nullIfMissing && nullIfPresent) {
                return res;
            } else {
                assert !nullIfMissing && !nullIfPresent;
                return getNonNullColl(null);
            }
        } else if (nullIfMissing) {
            assert !nullIfPresent;
            if (!this.containsKey(key))
                return null;
            else
                return getNonNullColl(super.get(key));
        } else {
            assert !nullIfMissing && nullIfPresent;
            if (this.containsKey(key))
                return super.get(key);
            else
                return getNonNullColl(null);
        }
    }

    @Override
    public final C getCollection(final Object key) {
        return this.get(key, !this.isEmptyCollSameAsNoColl(), true);
    }

    @Override
    public boolean containsInCollection(K key, V val) throws NullPointerException {
        final C c = this.get(key);
        if (c != null)
            return c.contains(val);
        else if (!this.containsKey(key))
            return false;
        else if (getMode() == Mode.NULL_MEANS_ALL)
            return true;
        else
            throw new NullPointerException("Null value for " + key);
    }

    /**
     * Returns a {@link Collection} view of all the values contained in this map. The collection is
     * backed by the map, so changes to the map are reflected in the collection, and vice-versa. If
     * the map is modified while an iteration over the collection is in progress (except through the
     * iterator's own <tt>remove</tt> operation), the results of the iteration are undefined. The
     * collection supports element removal, which removes the corresponding values from the map, via
     * the <tt>Iterator.remove</tt>, <tt>Collection.remove</tt>, <tt>removeAll</tt>,
     * <tt>retainAll</tt> and <tt>clear</tt> operations. Note that it doesn't remove entries only
     * values : keySet() doesn't change, use {@link #removeAllEmptyCollections()} and
     * {@link #removeAllNullCollections()} afterwards. It does not support the <tt>add</tt> or
     * <tt>addAll</tt> operations.
     * 
     * @return a view all values in all entries, <code>null</code> collections are ignored.
     */
    @Override
    public Collection<V> allValues() {
        if (this.allValues == null)
            this.allValues = new AllValues();
        return this.allValues;
    }

    private final class AllValues extends AbstractCollection<V> {
        @Override
        public Iterator<V> iterator() {
            return new AllValuesIterator();
        }

        @Override
        public int size() {
            int compt = 0;
            for (final C c : values()) {
                if (c != null)
                    compt += c.size();
            }
            return compt;
        }

        // don't overload clear() to call Map.clear() as this would be incoherent with removeAll() :
        // this last method only removes values, resulting in empty and null collections
    }

    private final class AllValuesIterator implements Iterator<V> {
        private final Iterator<C> mapIterator;
        private Iterator<V> tempIterator;

        private AllValuesIterator() {
            this.mapIterator = values().iterator();
            this.tempIterator = null;
        }

        private boolean searchNextIterator() {
            // tempIterator == null initially and when a collection is null
            while (this.tempIterator == null || !this.tempIterator.hasNext()) {
                if (!this.mapIterator.hasNext()) {
                    return false;
                }
                final C nextCol = this.mapIterator.next();
                this.tempIterator = nextCol == null ? null : nextCol.iterator();
            }
            return true;
        }

        @Override
        public boolean hasNext() {
            return searchNextIterator();
        }

        @Override
        public V next() {
            // search next iterator if necessary
            if (!hasNext())
                throw new NoSuchElementException();
            return this.tempIterator.next();
        }

        @Override
        public void remove() {
            if (this.tempIterator == null)
                throw new IllegalStateException();
            this.tempIterator.remove();
        }
    }

    @Override
    public Set<Map.Entry<K, C>> entrySet() {
        if (getMode() == Mode.NULL_FORBIDDEN) {
            // prevent null insertion
            // MAYBE cache
            return new EntrySet(super.entrySet());
        } else {
            return super.entrySet();
        }
    }

    private final class EntrySet extends AbstractCollection<Map.Entry<K, C>> implements Set<Map.Entry<K, C>> {

        private final Set<Map.Entry<K, C>> delegate;

        public EntrySet(final Set<java.util.Map.Entry<K, C>> delegate) {
            super();
            this.delegate = delegate;
        }

        @Override
        public int size() {
            return this.delegate.size();
        }

        @Override
        public boolean contains(final Object o) {
            return this.delegate.contains(o);
        }

        @Override
        public boolean remove(final Object o) {
            return this.delegate.remove(o);
        }

        @Override
        public void clear() {
            this.delegate.clear();
        }

        @Override
        public Iterator<Map.Entry<K, C>> iterator() {
            return new Iterator<Map.Entry<K, C>>() {

                private final Iterator<Map.Entry<K, C>> delegateIter = EntrySet.this.delegate.iterator();

                @Override
                public boolean hasNext() {
                    return this.delegateIter.hasNext();
                }

                @Override
                public Map.Entry<K, C> next() {
                    final Map.Entry<K, C> delegate = this.delegateIter.next();
                    return new Map.Entry<K, C>() {
                        @Override
                        public K getKey() {
                            return delegate.getKey();
                        }

                        @Override
                        public C getValue() {
                            return delegate.getValue();
                        }

                        @Override
                        public C setValue(final C value) {
                            if (value == null)
                                throw new NullPointerException("Putting null collection for " + toStr(getKey()));
                            return delegate.setValue(value);
                        }
                    };
                }

                @Override
                public void remove() {
                    this.delegateIter.remove();
                }
            };
        }

        @Override
        public boolean equals(final Object o) {
            return this.delegate.equals(o);
        }

        @Override
        public int hashCode() {
            return this.delegate.hashCode();
        }

        @Override
        public boolean removeAll(final Collection<?> c) {
            return this.delegate.removeAll(c);
        }
    }

    @Override
    public final C put(final K key, final C value) {
        return this.putCollection(key, value);
    }

    // copy passed collection
    @Override
    public final C putCollection(final K key, final Collection<? extends V> value) {
        if (value == null && this.getMode() == Mode.NULL_FORBIDDEN)
            throw new NullPointerException("Putting null collection for " + toStr(key));
        return super.put(key, value == null ? null : createCollection(value));
    }

    @SafeVarargs
    public final C putCollection(final K key, final V... value) {
        return this.putCollection(key, Arrays.asList(value));
    }

    public void putAllCollections(final Map<? extends K, ? extends Collection<? extends V>> m) {
        this.putAllCollections(m, false);
    }

    public void putAllCollections(final Map<? extends K, ? extends Collection<? extends V>> m, final boolean removeEmptyCollections) {
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : m.entrySet()) {
            if (!removeEmptyCollections || !e.getValue().isEmpty())
                this.putCollection(e.getKey(), e.getValue());
        }
    }

    // ** add/remove collection

    @Override
    public final boolean add(final K k, final V v) {
        return this.addAll(k, Collections.singleton(v));
    }

    @SafeVarargs
    public final boolean addAll(final K k, final V... v) {
        return this.addAll(k, Arrays.asList(v));
    }

    @Override
    public final boolean addAll(final K k, final Collection<? extends V> v) {
        final boolean nullIsAll = getMode() == Mode.NULL_MEANS_ALL;
        if (v == null && !nullIsAll)
            throw new NullPointerException("Adding null collection for " + toStr(k));
        final boolean containsKey = this.containsKey(k);
        if (v == null) {
            return this.putCollection(k, v) != null;
        } else if (!containsKey) {
            this.putCollection(k, v);
            return true;
        } else {
            final C currentColl = this.get(k);
            if (nullIsAll && currentColl == null) {
                // ignore since we can't add something to everything
                return false;
            } else {
                // will throw if currentCol is null
                return currentColl.addAll(v);
            }
        }
    }

    @Override
    public final void merge(final Map<? extends K, ? extends Collection<? extends V>> mm) {
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : mm.entrySet()) {
            this.addAll(e.getKey(), e.getValue());
        }
    }

    @Override
    public final void mergeScalarMap(final Map<? extends K, ? extends V> scalarMap) {
        for (final Map.Entry<? extends K, ? extends V> e : scalarMap.entrySet()) {
            this.add(e.getKey(), e.getValue());
        }
    }

    @Override
    public final boolean removeOne(final K k, final V v) {
        return this.remove(k, null, v, false, true);
    }

    @Override
    public final boolean removeAllInstancesOfItem(final K k, final V v) {
        return this.removeAll(k, Collections.singleton(v));
    }

    @Override
    public final boolean removeAll(final K k, final Collection<? extends V> coll) {
        return remove(k, coll, null, true, true);
    }

    private final boolean remove(final K k, final Collection<? extends V> coll, final V item, final boolean removeAll, final boolean removeEmptyColl) {
        assert removeAll && item == null || !removeAll && coll == null : "Non null value ignored";
        if (!this.containsKey(k) || removeAll && coll != null && coll.isEmpty())
            return false;
        boolean removeK = false;
        boolean modified = false;
        if (getMode() == Mode.NULL_MEANS_ALL) {
            if (removeAll && coll == null) {
                removeK = true;
            } else {
                final C currentColl = this.get(k);
                if (currentColl == null)
                    throw new IllegalStateException("Cannot remove from all for " + toStr(k));
                if (removeAll)
                    modified = currentColl.removeAll(coll);
                else
                    modified = currentColl.remove(item);
                if (removeEmptyColl && currentColl.isEmpty())
                    removeK = true;
            }
        } else {
            final C currentColl = this.get(k);
            // like addAll(), will throw if currentCol is null
            if (removeAll) {
                modified = currentColl.removeAll(coll);
            } else {
                modified = currentColl.remove(item);
            }
            if (removeEmptyColl && currentColl.isEmpty())
                removeK = true;
        }
        if (removeK) {
            this.remove(k);
            // since we just tested containsKey()
            modified = true;
        }
        return modified;
    }

    @Override
    public final boolean removeAll(final Map<? extends K, ? extends Collection<? extends V>> mm) {
        // allow mm.removeAll(mm)
        if (this == mm) {
            if (this.isEmpty())
                return false;
            this.clear();
            return true;
        }
        boolean modified = false;
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : mm.entrySet()) {
            modified |= this.removeAll(e.getKey(), e.getValue());
        }
        return modified;
    }

    @Override
    public final boolean removeAllScalar(final Map<? extends K, ? extends V> m) {
        boolean modified = false;
        // incompatible types, allowing removal without ConcurrentModificationException
        assert m != this;
        for (final Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            modified |= this.removeAllInstancesOfItem(e.getKey(), e.getValue());
        }
        return modified;
    }

    // ** remove empty/null collections

    public final C removeIfEmpty(final K k) {
        final C v = this.get(k);
        if (v != null && v.isEmpty())
            return this.remove(k);
        else
            return null;
    }

    public final void removeIfNull(final K k) {
        if (this.get(k) == null)
            this.remove(k);
    }

    @Override
    public final Set<K> removeAllEmptyCollections() {
        return this.removeAll(true);
    }

    @Override
    public final Set<K> removeAllNullCollections() {
        return this.removeAll(false);
    }

    private final Set<K> removeAll(final boolean emptyOrNull) {
        final Set<K> removed = new HashSet<K>();
        final Iterator<Map.Entry<K, C>> iter = this.entrySet().iterator();
        while (iter.hasNext()) {
            final Map.Entry<K, C> e = iter.next();
            final C val = e.getValue();
            if ((emptyOrNull && val != null && val.isEmpty()) || (!emptyOrNull && val == null)) {
                iter.remove();
                removed.add(e.getKey());
            }
        }
        return removed;
    }

    public abstract C createCollection(Collection<? extends V> v);

    @Override
    public int hashCode() {
        if (this.mode == Mode.NULL_MEANS_ALL)
            return this.hashCodeExact();
        else
            return super.hashCode();
    }

    public int hashCodeExact() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + (this.emptyCollSameAsNoColl ? 1231 : 1237);
        result = prime * result + this.mode.hashCode();
        return result;
    }

    /**
     * Compares the specified object with this map for equality. Except for
     * {@link Mode#NULL_MEANS_ALL}, returns <tt>true</tt> if the given object is also a map and the
     * two maps represent the same mappings (as required by {@link Map}).
     * <code>NULL_MEANS_ALL</code> maps are tested using {@link #equalsExact(Object)}, meaning they
     * don't conform to the Map interface.
     * 
     * @param obj object to be compared for equality with this map
     * @return <tt>true</tt> if the specified object is equal to this map
     * @see #equalsExact(Object)
     */
    @Override
    public final boolean equals(final Object obj) {
        return this.equals(obj, false);
    }

    /**
     * Compares the specified object with this map for complete equality. This method not only
     * checks for equality of values (as required by {@link Map}) but also the class and attributes.
     * 
     * @param obj object to be compared for equality with this map
     * @return <tt>true</tt> if the specified object is exactly equal to this map.
     */
    public final boolean equalsExact(final Object obj) {
        return this.equals(obj, true);
    }

    private final boolean equals(final Object obj, final boolean forceExact) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        assert obj != null;
        final CollectionMap2<?, ?, ?> other = obj instanceof CollectionMap2 ? (CollectionMap2<?, ?, ?>) obj : null;
        if (forceExact || this.mode == Mode.NULL_MEANS_ALL || (other != null && other.mode == Mode.NULL_MEANS_ALL)) {
            if (getClass() != obj.getClass())
                return false;
            // no need to test createCollection(), since values are tested by super.equals()
            return this.emptyCollSameAsNoColl == other.emptyCollSameAsNoColl && this.mode == other.mode && this.getDelegate().getClass() == other.getDelegate().getClass();
        } else {
            return true;
        }
    }

    @Override
    public CollectionMap2<K, C, V> clone() {
        @SuppressWarnings("unchecked")
        final CollectionMap2<K, C, V> result = (CollectionMap2<K, C, V>) super.clone();
        // allValues has a reference to this
        result.allValues = null;
        // clone each collection value
        for (final Map.Entry<K, C> entry : result.entrySet()) {
            final C coll = entry.getValue();
            entry.setValue(createCollection(coll));
        }
        return result;
    }
}