/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.geotoolkit.util.collection; import java.util.Map; import java.util.Set; import java.util.Iterator; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.NoSuchElementException; import org.apache.sis.util.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * A {@linkplain Collections#checkedMap(Map, Class, Class) checked} and * {@linkplain Collections#synchronizedMap(Map) synchronized} {@link LinkedHashMap}. * The type checks are performed at run-time in addition to the compile-time checks. * * <p>Using this class is similar to wrapping a {@link LinkedHashMap} using the methods provided * in the standard {@link Collections} class, except for the following advantages:</p> * * <ul> * <li>Avoid the two levels of indirection (for type check and synchronization).</li> * <li>Checks for write permission.</li> * <li>Overrideable methods for controlling the synchronization lock and write permission checks.</li> * </ul> * * The synchronization is provided mostly in order to prevent damages * to the map in case of concurrent access. It does <strong>not</strong> prevent * {@link java.util.ConcurrentModificationException} to be thrown during iterations, * unless the whole iteration is synchronized on this map {@linkplain #getLock() lock}. * For real concurrency, see the {@link java.util.concurrent} package instead. * * {@note The above is the reason why the name of this class emphases the <cite>checked</cite> * aspect rather than the <cite>synchronized</cite> aspect of the map.} * * @param <K> The type of keys in the map. * @param <V> The type of values in the map. * * @author Martin Desruisseaux (Geomatys) * @since 2.1 * @version 3.22 * @module * * @see Collections#checkedMap(Map, Class, Class) * @see Collections#synchronizedMap(Map) */ public class CheckedHashMap<K,V> extends LinkedHashMap<K,V> implements Cloneable { /** * Serial version UID for compatibility with different versions. */ private static final long serialVersionUID = -5983828895888575736L; /** * The class type for keys. */ private final Class<K> keyType; /** * The class type for values. */ private final Class<V> valueType; /** * Constructs a map of the specified key and value types. * * @param keyType The key type (can not be null). * @param valueType The value type (can not be null). */ public CheckedHashMap(final Class<K> keyType, final Class<V> valueType) { this.keyType = keyType; this.valueType = valueType; ensureNonNull("keyType", keyType); ensureNonNull("valueType", valueType); } /** * Checks the type of the specified object. * * @param element the object to check, or {@code null}. * @throws IllegalArgumentException if the specified element is not of the expected type. */ private static <E> void ensureValidType(final E element, final Class<E> type) throws IllegalArgumentException { if (element!=null && !type.isInstance(element)) { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalArgumentClass_3, "element", type, element.getClass())); } } /** * Checks if changes in this map are allowed. This method is automatically invoked * after this map got the {@linkplain #getLock() lock} and before any operation that * may change the content. If the write operation is allowed, then this method shall * returns normally. Otherwise an {@link UnsupportedOperationException} is thrown. * * <p>The default implementation does nothing significant (see below), thus allowing this map to * be modified. Subclasses can override this method if they want to control write permissions.</p> * * {@note Actually the current implementation contains an <code>assert</code> statement * ensuring that the thread holds the lock. This is an implementation details that may * change in any future version of the SIS library. Nevertheless methods that override * this one are encouraged to invoke <code>super.checkWritePermission()</code>.} * * @throws UnsupportedOperationException if this map is unmodifiable. */ protected void checkWritePermission() throws UnsupportedOperationException { assert Thread.holdsLock(getLock()); } /** * Returns the synchronization lock. The default implementation returns {@code this}. * * {@section Note for subclass implementors} * Subclasses that override this method must be careful to update the lock reference * (if needed) when this map is {@linkplain #clone() cloned}. * * @return The synchronization lock. */ protected Object getLock() { return this; } /** * Returns the number of elements in this map. */ @Override public int size() { synchronized (getLock()) { return super.size(); } } /** * Returns {@code true} if this map contains no elements. */ @Override public boolean isEmpty() { synchronized (getLock()) { return super.isEmpty(); } } /** * Returns {@code true} if this map contains the specified key. */ @Override public boolean containsKey(final Object key) { synchronized (getLock()) { return super.containsKey(key); } } /** * Returns {@code true} if this map contains the specified value. */ @Override public boolean containsValue(final Object value) { synchronized (getLock()) { return super.containsValue(value); } } /** * Returns the value to which the specified key is mapped, or {@code null} if none. */ @Override public V get(Object key) { synchronized (getLock()) { return super.get(key); } } /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for this key, the old * value is replaced. * * @param key key with which the specified value is to be associated. * @param value value to be associated with the specified key. * @return previous value associated with specified key, or {@code null}. * @throws IllegalArgumentException if the key or the value is not of the expected type. * @throws UnsupportedOperationException if this collection is unmodifiable. */ @Override public V put(final K key, final V value) throws IllegalArgumentException, UnsupportedOperationException { ensureValidType(key, keyType); ensureValidType(value, valueType); synchronized (getLock()) { checkWritePermission(); return super.put(key, value); } } /** * Copies all of the mappings from the specified map to this map. * * @throws UnsupportedOperationException if this collection is unmodifiable. */ @Override public void putAll(Map<? extends K, ? extends V> m) throws UnsupportedOperationException { for (final Map.Entry<? extends K, ? extends V> entry : m.entrySet()) { ensureValidType(entry.getKey(), keyType); ensureValidType(entry.getValue(), valueType); } synchronized (getLock()) { checkWritePermission(); super.putAll(m); } } /** * Removes the mapping for the specified key from this map if present. * * @throws UnsupportedOperationException if this collection is unmodifiable. */ @Override public V remove(Object key) throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); return super.remove(key); } } /** * Removes all of the elements from this map. * * @throws UnsupportedOperationException if this collection is unmodifiable. */ @Override public void clear() throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); super.clear(); } } /** * Returns a view of the keys in the map. * The returned set will support {@linkplain Set#remove(Object) element removal} * only if the {@link #checkWritePermission()} method does not throw exception. * * @return A synchronized view of the keys in the map. */ @Override public Set<K> keySet() { synchronized (getLock()) { return new SyncSet<>(super.keySet()); } } /** * Returns a view of the values in the map. * The returned collection will support {@linkplain Collection#remove(Object) element removal} * only if the {@link #checkWritePermission()} method does not throw exception. * * @return A synchronized view of the values in the map. */ @Override public Collection<V> values() { synchronized (getLock()) { return new Sync<>(super.values()); } } /** * Returns a view of the entries in the map. * The returned set will support {@linkplain Set#remove(Object) element removal} * only if the {@link #checkWritePermission()} method does not throw exception. * * @return A synchronized view of the keys in the map. */ @Override public Set<Map.Entry<K,V>> entrySet() { synchronized (getLock()) { return new SyncSet<>(super.entrySet()); } } /** * Returns a string representation of this map. */ @Override public String toString() { synchronized (getLock()) { return super.toString(); } } /** * Compares the specified object with this map for equality. */ @Override public boolean equals(Object o) { synchronized (getLock()) { return super.equals(o); } } /** * Returns the hash code value for this map. */ @Override public int hashCode() { synchronized (getLock()) { return super.hashCode(); } } /** * Returns a shallow copy of this map. * * @return A shallow copy of this map. */ @Override @SuppressWarnings("unchecked") public CheckedHashMap<K,V> clone() { synchronized (getLock()) { return (CheckedHashMap<K,V>) super.clone(); } } /** * A synchronized iterator with a check for write permission prior element removal. * This class wraps the iterator provided by the {@link LinkedHashMap} views. */ private final class Iter<E> implements Iterator<E> { /** The {@link LinkedHashMap} iterator. */ private final Iterator<E> iterator; /** Creates a new wrapper for the given {@link LinkedHashMap} iterator. */ Iter(final Iterator<E> iterator) { this.iterator = iterator; } /** Returns {@code true} if there is more elements in the iteration. */ @Override public boolean hasNext() { synchronized (getLock()) { return iterator.hasNext(); } } /** Returns the next element in the iteration. */ @Override public E next() throws NoSuchElementException { synchronized (getLock()) { return iterator.next(); } } /** Removes the previous element if the enclosing {@link CheckedHashMap} allows write operations. */ @Override public void remove() throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); iterator.remove(); } } } /** * A collection or a set synchronized on the enclosing map {@linkplain #getLock() lock}. * This is used directly for wrapping {@link Map#values()}, or indirectly for wrapping * {@link Map#keySet()} or {@link Map#entrySet()} views. */ private class Sync<E> implements Collection<E> { /** The {@link Map#keySet()}, {@link Map#values()} or {@link Map#entrySet()} view. */ private final Collection<E> view; /** Create a new synchronized wrapper for the given view. */ Sync(final Collection<E> view) { this.view = view; } /** Returns a synchronized and checked iterator over the elements in this collection. */ @Override public final Iterator<E> iterator() { synchronized (getLock()) { return new Iter<>(view.iterator()); } } /** Returns the number of elements in the collection. */ @Override public final int size() { synchronized (getLock()) { return view.size(); } } /** Returns {@code true} if the collection is empty. */ @Override public final boolean isEmpty() { synchronized (getLock()) { return view.isEmpty(); } } /** Returns {@code true} if the collection contains the given element. */ @Override public final boolean contains(final Object element) { synchronized (getLock()) { return view.contains(element); } } /** Returns {@code true} if the collection contains all elements of the given collection. */ @Override public final boolean containsAll(final Collection<?> collection) { synchronized (getLock()) { return view.containsAll(collection); } } /** Always unsupported operation in hash map views. */ @Override public final boolean add(final E element) throws UnsupportedOperationException { throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, "add")); } /** Always unsupported operation in hash map views. */ @Override public final boolean addAll(final Collection<? extends E> collection) throws UnsupportedOperationException { throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, "addAll")); } /** Remove the given element if the enclosing {@link CheckedHashMap} supports write operations. */ @Override public final boolean remove(final Object element) throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); return view.remove(element); } } /** Remove the given elements if the enclosing {@link CheckedHashMap} supports write operations. */ @Override public final boolean removeAll(final Collection<?> collection) throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); return view.removeAll(collection); } } /** Retains only the given elements if the enclosing {@link CheckedHashMap} supports write operations. */ @Override public final boolean retainAll(final Collection<?> collection) throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); return view.retainAll(collection); } } /** Removes all elements from the collection. */ @Override public final void clear() throws UnsupportedOperationException { synchronized (getLock()) { checkWritePermission(); view.clear(); } } /** Returns the elements in an array. */ @Override public final Object[] toArray() { synchronized (getLock()) { return view.toArray(); } } /** Returns the elements in an array. */ @Override public final <T> T[] toArray(final T[] array) { synchronized (getLock()) { return view.toArray(array); } } /** Returns a string representation of the elements. */ @Override public final String toString() { synchronized (getLock()) { return view.toString(); } } /** Compare this collection with the given object for equality. */ @Override public final boolean equals(final Object other) { synchronized (getLock()) { return view.equals(other); } } /** Returns a hash code value for this collection. */ @Override public final int hashCode() { synchronized (getLock()) { return view.hashCode(); } } } /** * A set synchronized on the enclosing map {@linkplain #getLock() lock}. * This is used for wrapping {@link Map#keySet()} or {@link Map#entrySet()} views. */ private final class SyncSet<E> extends Sync<E> implements Set<E> { /** Create a new synchronized wrapper for the given view. */ SyncSet(final Set<E> set) { super(set); } } }