// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.openstreetmap.josm.tools.Utils;
/**
* A Set-like class that allows looking up equivalent preexising instance.
* It is useful whereever one would use self-mapping construct like
* Map<T,T>.put(t,t)
, that is, for caches, uniqueness filters or similar.
*
* The semantics of equivalency can be external to the object, using the
* {@link Hash} interface. The set also supports querying for entries using
* different key type, in case you can provide a Hash implementation
* that can resolve the equality.
*
*
Examples
* - A String cache:
*
* Storage<String> cache = new Storage(); // use default Hash
* for (String input : data) {
* String onlyOne = cache.putIfUnique(input);
* ....
* }
*
* - Identity-based set:
*
* Storage<Object> identity = new Storage(new Hash<Object,Object> {
* public int getHashCode(Object o) {
* return System.identityHashCode(o);
* }
* public boolean equals(Object o1, Object o2) {
* return o1 == o2;
* }
* });
*
* - An object with int ID and id-based lookup:
*
* class Thing { int id; }
* Storage<Thing> things = new Storage(new Hash<Thing,Thing>() {
* public int getHashCode(Thing t) {
* return t.id;
* }
* public boolean equals(Thing t1, Thing t2) {
* return t1 == t2;
* }
* });
* Map<Integer,Thing> fk = things.foreignKey(new Hash<Integer,Thing>() {
* public int getHashCode(Integer i) {
* return i.getIntValue();
* }
* public boolean equals(Integer k, Thing t) {
* return t.id == k.getIntvalue();
* }
* }
*
* things.put(new Thing(3));
* assert things.get(new Thing(3)) == fk.get(3);
*
*
*
* @author nenik
* @param type of stored objects
*/
public class Storage extends AbstractSet {
/**
* Hash for {@link PrimitiveId}.
*/
public static class PrimitiveIdHash implements Hash {
@Override
public int getHashCode(PrimitiveId k) {
return (int) k.getUniqueId() ^ k.getType().hashCode();
}
@Override
public boolean equals(PrimitiveId key, PrimitiveId value) {
if (key == null || value == null) return false;
return key.getUniqueId() == value.getUniqueId() && key.getType() == value.getType();
}
}
private final Hash super T, ? super T> hash;
private T[] data;
private int mask;
private int size;
private volatile int modCount;
private static final double LOAD_FACTOR = 0.6d;
private static final int DEFAULT_CAPACITY = 16;
private final boolean safeIterator;
private boolean arrayCopyNecessary;
/**
* Constructs a new {@code Storage} with default capacity (16).
*/
public Storage() {
this(Storage.defaultHash(), DEFAULT_CAPACITY, false);
}
/**
* Constructs a new {@code Storage} with given capacity.
* @param capacity capacity
*/
public Storage(int capacity) {
this(Storage.defaultHash(), capacity, false);
}
/**
* Constructs a new {@code Storage} with given hash.
* @param ha hash
*/
public Storage(Hash super T, ? super T> ha) {
this(ha, DEFAULT_CAPACITY, false);
}
/**
* Constructs a new {@code Storage}.
* @param safeIterator If set to false, you must not modify the Storage while iterating over it.
* If set to true, you can safely modify, but the read-only iteration will happen on a copy of the unmodified Storage.
* This is similar to CopyOnWriteArrayList.
*/
public Storage(boolean safeIterator) {
this(Storage.defaultHash(), DEFAULT_CAPACITY, safeIterator);
}
/**
* Constructs a new {@code Storage} with given capacity.
* @param capacity capacity
* @param safeIterator If set to false, you must not modify the Storage while iterating over it.
* If set to true, you can safely modify, but the read-only iteration will happen on a copy of the unmodified Storage.
* This is similar to CopyOnWriteArrayList.
*/
public Storage(int capacity, boolean safeIterator) {
this(Storage.defaultHash(), capacity, safeIterator);
}
/**
* Constructs a new {@code Storage} with given hash.
* @param ha hash
* @param safeIterator If set to false, you must not modify the Storage while iterating over it.
* If set to true, you can safely modify, but the read-only iteration will happen on a copy of the unmodified Storage.
* This is similar to CopyOnWriteArrayList.
*/
public Storage(Hash super T, ? super T> ha, boolean safeIterator) {
this(ha, DEFAULT_CAPACITY, safeIterator);
}
/**
* Constructs a new {@code Storage} with given hash and capacity.
* @param ha hash
* @param capacity capacity
*/
public Storage(Hash super T, ? super T> ha, int capacity) {
this(ha, capacity, false);
}
/**
* Constructs a new {@code Storage} with given hash and capacity.
* @param ha hash
* @param capacity capacity
* @param safeIterator If set to false, you must not modify the Storage while iterating over it.
* If set to true, you can safely modify, but the read-only iteration will happen on a copy of the unmodified Storage.
* This is similar to CopyOnWriteArrayList.
*/
public Storage(Hash super T, ? super T> ha, int capacity, boolean safeIterator) {
this.hash = ha;
int cap = 1 << (int) (Math.ceil(Math.log(capacity/LOAD_FACTOR) / Math.log(2)));
@SuppressWarnings("unchecked")
T[] newData = (T[]) new Object[cap];
data = newData;
mask = data.length - 1;
this.safeIterator = safeIterator;
}
private void copyArray() {
if (arrayCopyNecessary) {
data = Utils.copyArray(data);
arrayCopyNecessary = false;
}
}
// --------------- Collection implementation ------------------------
@Override
public synchronized int size() {
return size;
}
@Override
public synchronized Iterator iterator() {
if (safeIterator) {
arrayCopyNecessary = true;
return new SafeReadonlyIter(data);
} else
return new Iter();
}
@Override
public synchronized boolean contains(Object o) {
@SuppressWarnings("unchecked")
T t = (T) o;
int bucket = getBucket(hash, t);
return bucket >= 0;
}
@Override
public synchronized boolean add(T t) {
T orig = putUnique(t);
return orig == t;
}
@Override
public synchronized boolean remove(Object o) {
@SuppressWarnings("unchecked")
T t = (T) o;
T tOrig = removeElem(t);
return tOrig != null;
}
@Override
public synchronized void clear() {
copyArray();
modCount++;
size = 0;
for (int i = 0; i < data.length; i++) {
data[i] = null;
}
}
@Override
public synchronized int hashCode() {
int h = 0;
if (hash != null) {
for (T t : this) {
h += hash.getHashCode(t);
}
}
return h;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Storage> other = (Storage>) obj;
return Arrays.equals(data, other.data)
&& hashCode() == obj.hashCode();
}
// ----------------- Extended API ----------------------------
public synchronized T put(T t) {
copyArray();
modCount++;
ensureSpace();
int bucket = getBucket(hash, t);
if (bucket < 0) {
size++;
bucket = ~bucket;
assert data[bucket] == null;
}
T old = data[bucket];
data[bucket] = t;
return old;
}
public synchronized T get(T t) {
int bucket = getBucket(hash, t);
return bucket < 0 ? null : data[bucket];
}
public synchronized T putUnique(T t) {
copyArray();
modCount++;
ensureSpace();
int bucket = getBucket(hash, t);
if (bucket < 0) { // unique
size++;
assert data[~bucket] == null;
data[~bucket] = t;
return t;
}
return data[bucket];
}
public synchronized T removeElem(T t) {
copyArray();
modCount++;
int bucket = getBucket(hash, t);
return bucket < 0 ? null : doRemove(bucket);
}
public Map foreignKey(Hash h) {
return new FMap<>(h);
}
// ---------------- Implementation
/**
* Additional mixing of hash
* @param h hash
* @return new hash
*/
private static int rehash(int h) {
return (1_103_515_245*h) >> 2;
}
/**
* Finds a bucket for given key.
* @param type for hashCode and first equals parameter
* @param ha hash function
*
* @param key The key to compare
* @return the bucket equivalent to the key or -(bucket) as an empty slot
* where such an entry can be stored.
*/
private int getBucket(Hash ha, K key) {
T entry;
int hcode = rehash(ha.getHashCode(key));
int bucket = hcode & mask;
while (bucket < data.length && (entry = data[bucket]) != null) {
if (ha.equals(key, entry))
return bucket;
bucket = (bucket+1) & mask;
}
return ~bucket;
}
private T doRemove(int slot) {
T t = data[slot];
assert t != null;
fillTheHole(slot); // fill the hole (or null it)
size--;
return t;
}
private void fillTheHole(int hole) {
int bucket = (hole+1) & mask;
T entry;
while ((entry = data[bucket]) != null) {
int right = rehash(hash.getHashCode(entry)) & mask;
// if the entry should be in (circular-wise)
// we can't move it. The move can be proved safe otherwise,
// because the entry safely belongs to
if ((bucket < right && (right <= hole || hole <= bucket)) ||
(right <= hole && hole <= bucket)) {
data[hole] = data[bucket];
hole = bucket;
}
bucket = (bucket+1) & mask;
}
// no entry belongs here, just null out the slot
data[hole] = null;
}
private void ensureSpace() {
if (size > data.length*LOAD_FACTOR) { // rehash
@SuppressWarnings("unchecked")
T[] big = (T[]) new Object[data.length * 2];
int nMask = big.length - 1;
for (T o : data) {
if (o == null) {
continue;
}
int bucket = rehash(hash.getHashCode(o)) & nMask;
while (big[bucket] != null) {
bucket = (bucket+1) & nMask;
}
big[bucket] = o;
}
data = big;
mask = nMask;
}
}
// -------------- factories --------------------
/**
* A factory for default hash implementation.
* @param type for hash
* @return a hash implementation that just delegates to object's own hashCode and equals.
*/
public static Hash defaultHash() {
return new Hash() {
@Override
public int getHashCode(O t) {
return t.hashCode();
}
@Override
public boolean equals(O t1, O t2) {
return t1.equals(t2);
}
};
}
private final class FMap implements Map {
private final Hash fHash;
private FMap(Hash h) {
fHash = h;
}
@Override
public int size() {
return Storage.this.size();
}
@Override
public boolean isEmpty() {
return Storage.this.isEmpty();
}
@Override
public boolean containsKey(Object o) {
@SuppressWarnings("unchecked")
K key = (K) o;
int bucket = getBucket(fHash, key);
return bucket >= 0;
}
@Override
public boolean containsValue(Object value) {
return Storage.this.contains(value);
}
@Override
public T get(Object o) {
@SuppressWarnings("unchecked")
K key = (K) o;
int bucket = getBucket(fHash, key);
return bucket < 0 ? null : data[bucket];
}
@Override
public T put(K key, T value) {
if (!fHash.equals(key, value)) throw new IllegalArgumentException("inconsistent key");
return Storage.this.put(value);
}
@Override
public T remove(Object o) {
synchronized (Storage.this) {
modCount++;
@SuppressWarnings("unchecked")
K key = (K) o;
int bucket = getBucket(fHash, key);
return bucket < 0 ? null : doRemove(bucket);
}
}
@Override
public void putAll(Map extends K, ? extends T> m) {
if (m instanceof Storage.FMap) {
Storage.this.addAll(m.values());
} else {
for (Map.Entry extends K, ? extends T> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
}
@Override
public void clear() {
Storage.this.clear();
}
@Override
public Set keySet() {
throw new UnsupportedOperationException();
}
@Override
public Collection values() {
return Storage.this;
}
@Override
public Set> entrySet() {
throw new UnsupportedOperationException();
}
}
private abstract class AbstractIter implements Iterator {
protected int slot;
protected final boolean doHasNext(T[] data) {
if (data == null) return false;
align(data);
return slot < data.length;
}
protected void align(T[] data) {
while (slot < data.length && data[slot] == null) {
slot++;
}
}
}
private final class SafeReadonlyIter extends AbstractIter {
private final T[] data;
SafeReadonlyIter(T[] data) {
this.data = data;
}
@Override
public boolean hasNext() {
return doHasNext(data);
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
return data[slot++];
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private final class Iter extends AbstractIter {
private final int mods;
private int removeSlot = -1;
Iter() {
mods = modCount;
}
@Override
public boolean hasNext() {
return doHasNext(data);
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
removeSlot = slot;
return data[slot++];
}
@Override
public void remove() {
if (removeSlot == -1) throw new IllegalStateException();
doRemove(removeSlot);
slot = removeSlot; // some entry might have been relocated here
removeSlot = -1;
}
@Override
protected void align(T[] data) {
if (mods != modCount)
throw new ConcurrentModificationException();
super.align(data);
}
}
}