/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed 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 com.intellij.util.containers;
import gnu.trove.THashMap;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import java.lang.ref.ReferenceQueue;
import java.util.*;
/**
* Base class for (soft/weak) keys -> hard values map
* Null keys are NOT allowed
* Null values are allowed
*/
abstract class RefHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {
private final MyMap myMap;
private final ReferenceQueue<K> myReferenceQueue = new ReferenceQueue<K>();
private final HardKey myHardKeyInstance = new HardKey(); // "singleton"
@NotNull
private final TObjectHashingStrategy<K> myStrategy;
private Set<Entry<K, V>> entrySet;
private boolean processingQueue;
RefHashMap(int initialCapacity, float loadFactor, @NotNull final TObjectHashingStrategy<K> strategy) {
myStrategy = strategy;
myMap = new MyMap(initialCapacity, loadFactor);
}
RefHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, ContainerUtil.<K>canonicalStrategy());
}
RefHashMap(int initialCapacity) {
this(initialCapacity, 0.8f);
}
RefHashMap() {
this(4);
}
RefHashMap(@NotNull Map<K, V> t) {
this(Math.max(2 * t.size(), 11), 0.75f);
putAll(t);
}
RefHashMap(@NotNull final TObjectHashingStrategy<K> hashingStrategy) {
this(4, 0.8f, hashingStrategy);
}
static <K> boolean keyEqual(K k1, K k2, TObjectHashingStrategy<K> strategy) {
return k1 == k2 || strategy.equals(k1, k2);
}
private class MyMap extends THashMap<Key<K>, V> {
private MyMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, new TObjectHashingStrategy<Key<K>>() {
@Override
public int computeHashCode(final Key<K> key) {
return key.hashCode(); // use stored hashCode
}
@Override
public boolean equals(final Key<K> o1, final Key<K> o2) {
return o1 == o2 || keyEqual(o1.get(), o2.get(), myStrategy);
}
});
}
@Override
public void compact() {
// do not compact the map during many gced references removal because it's bad for performance
if (!processingQueue) {
super.compact();
}
}
private void compactIfNecessary() {
if (_deadkeys > _size && capacity() > 42) {
// Compact if more than 50% of all keys are dead. Also, don't trash small maps
compact();
}
}
@Override
protected void rehash(int newCapacity) {
// rehash should discard gced keys
// because otherwise there is a remote probability of
// having two (Weak|Soft)Keys with accidentally equal hashCodes and different but gced key values
int oldCapacity = _set.length;
Object[] oldKeys = _set;
V[] oldVals = _values;
_set = new Object[newCapacity];
_values = (V[])new Object[newCapacity];
for (int i = oldCapacity; i-- > 0; ) {
Object o = oldKeys[i];
if (o == null || o == REMOVED) continue;
Key<K> k = (Key<K>)o;
K key = k.get();
if (key == null) continue;
int index = insertionIndex(k);
if (index < 0) {
throwObjectContractViolation(_set[-index - 1], o);
// make 'key' alive till this point to not allow 'o.referent' to be gced
if (key == _set) throw new AssertionError();
}
_set[index] = o;
_values[index] = oldVals[i];
}
}
}
interface Key<T> {
T get();
}
@NotNull
protected abstract <T> Key<T> createKey(@NotNull T k, @NotNull TObjectHashingStrategy<T> strategy, @NotNull ReferenceQueue<? super T> q);
private class HardKey implements Key<K> {
private K myObject;
private int myHash;
@Override
public K get() {
return myObject;
}
private void set(@NotNull K object) {
myObject = object;
myHash = myStrategy.computeHashCode(object);
}
private void clear() {
myObject = null;
}
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Key)) return false;
K t = myObject;
K u = ((Key<K>)o).get();
return keyEqual(t, u, myStrategy);
}
public int hashCode() {
return myHash;
}
}
// returns true if some refs were tossed
boolean processQueue() {
boolean processed = false;
try {
processingQueue = true;
Key<K> wk;
while ((wk = (Key<K>)myReferenceQueue.poll()) != null) {
removeKey(wk);
processed = true;
}
}
finally {
processingQueue = false;
}
myMap.compactIfNecessary();
return processed;
}
V removeKey(@NotNull Key<K> key) {
return myMap.remove(key);
}
@NotNull
Key<K> createKey(@NotNull K key) {
return createKey(key, myStrategy, myReferenceQueue);
}
V putKey(@NotNull Key<K> weakKey, V value) {
return myMap.put(weakKey, value);
}
@Override
public int size() {
return entrySet().size();
}
@Override
public boolean isEmpty() {
return entrySet().isEmpty();
}
@Override
public boolean containsKey(Object key) {
if (key == null) return false;
// optimization:
myHardKeyInstance.set((K)key);
boolean result = myMap.containsKey(myHardKeyInstance);
myHardKeyInstance.clear();
return result;
}
@Override
public V get(Object key) {
if (key == null) return null;
myHardKeyInstance.set((K)key);
V result = myMap.get(myHardKeyInstance);
myHardKeyInstance.clear();
return result;
}
@Override
public V put(@NotNull K key, V value) {
processQueue();
return putKey(createKey(key), value);
}
@Override
public V remove(@NotNull Object key) {
processQueue();
// optimization:
myHardKeyInstance.set((K)key);
V result = myMap.remove(myHardKeyInstance);
myHardKeyInstance.clear();
return result;
}
@Override
public void clear() {
processQueue();
myMap.clear();
}
private static class MyEntry<K, V> implements Entry<K, V> {
private final Entry<?, V> ent;
private final K key; // Strong reference to key, so that the GC will leave it alone as long as this Entry exists
private final int myKeyHashCode;
@NotNull private final TObjectHashingStrategy<K> myStrategy;
private MyEntry(@NotNull Entry<?, V> ent, @NotNull K key, int keyHashCode, @NotNull TObjectHashingStrategy<K> strategy) {
this.ent = ent;
this.key = key;
myKeyHashCode = keyHashCode;
myStrategy = strategy;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return ent.getValue();
}
@Override
public V setValue(V value) {
return ent.setValue(value);
}
private static boolean valEqual(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
public boolean equals(Object o) {
if (!(o instanceof Entry)) return false;
Entry<K,V> e = (Entry)o;
return keyEqual(key, e.getKey(), myStrategy) && valEqual(getValue(), e.getValue());
}
public int hashCode() {
V v;
return myKeyHashCode ^ ((v = getValue()) == null ? 0 : v.hashCode());
}
}
/* Internal class for entry sets */
private class EntrySet extends AbstractSet<Entry<K, V>> {
private final Set<Entry<Key<K>, V>> hashEntrySet = myMap.entrySet();
@NotNull
@Override
public Iterator<Entry<K, V>> iterator() {
return new Iterator<Entry<K, V>>() {
private final Iterator<Entry<Key<K>, V>> hashIterator = hashEntrySet.iterator();
private MyEntry<K, V> next;
@Override
public boolean hasNext() {
while (hashIterator.hasNext()) {
Entry<Key<K>, V> ent = hashIterator.next();
Key<K> wk = ent.getKey();
K k;
if ((k = wk.get()) == null) {
// weak key has been cleared by GC, ignore
continue;
}
next = new MyEntry<K, V>(ent, k, wk.hashCode(), myStrategy);
return true;
}
return false;
}
@Override
public Entry<K, V> next() {
if (next == null && !hasNext()) {
throw new NoSuchElementException();
}
Entry<K, V> e = next;
next = null;
return e;
}
@Override
public void remove() {
hashIterator.remove();
}
};
}
@Override
public boolean isEmpty() {
return !iterator().hasNext();
}
@Override
public int size() {
int j = 0;
for (Iterator i = iterator(); i.hasNext(); i.next()) j++;
return j;
}
@Override
public boolean remove(Object o) {
processQueue();
if (!(o instanceof Entry)) return false;
Entry<K, V> e = (Entry<K, V>)o;
V ev = e.getValue();
// optimization: do not recreate the key
myHardKeyInstance.set(e.getKey());
Key<K> key = myHardKeyInstance;
V hv = myMap.get(key);
boolean toRemove = hv == null ? ev == null && myMap.containsKey(key) : hv.equals(ev);
if (toRemove) {
myMap.remove(key);
}
myHardKeyInstance.clear();
return toRemove;
}
public int hashCode() {
int h = 0;
for (Entry<Key<K>,V> entry : hashEntrySet) {
Key<K> wk = entry.getKey();
if (wk == null) continue;
Object v;
h += wk.hashCode() ^ ((v = entry.getValue()) == null ? 0 : v.hashCode());
}
return h;
}
}
@NotNull
@Override
public Set<Entry<K, V>> entrySet() {
if (entrySet == null) entrySet = new EntrySet();
return entrySet;
}
@TestOnly
int underlyingMapSize() {
return myMap.size();
}
}