| /* |
| * Copyright (C) 2012 The Guava Authors |
| * |
| * 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.google.common.cache; |
| |
| import static com.google.common.base.MoreObjects.firstNonNull; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.common.base.Equivalence; |
| import com.google.common.base.Ticker; |
| import com.google.common.cache.AbstractCache.StatsCounter; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.util.concurrent.ExecutionError; |
| import com.google.common.util.concurrent.UncheckedExecutionException; |
| import java.util.AbstractCollection; |
| import java.util.AbstractSet; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.ExecutionException; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| |
| /** |
| * LocalCache emulation for GWT. |
| * |
| * @param <K> the base key type |
| * @param <V> the base value type |
| * @author Charles Fry |
| * @author Jon Donovan |
| */ |
| public class LocalCache<K, V> implements ConcurrentMap<K, V> { |
| private static final int UNSET_INT = CacheBuilder.UNSET_INT; |
| |
| private final LinkedHashMap<K, Timestamped<V>> cachingHashMap; |
| private final CacheLoader<? super K, V> loader; |
| private final RemovalListener removalListener; |
| private final StatsCounter statsCounter; |
| private final Ticker ticker; |
| private final long expireAfterWrite; |
| private final long expireAfterAccess; |
| |
| LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { |
| this.loader = loader; |
| this.removalListener = builder.removalListener; |
| this.expireAfterAccess = builder.expireAfterAccessNanos; |
| this.expireAfterWrite = builder.expireAfterWriteNanos; |
| this.statsCounter = builder.getStatsCounterSupplier().get(); |
| |
| /* Implements size-capped LinkedHashMap */ |
| final long maximumSize = builder.maximumSize; |
| this.cachingHashMap = |
| new CapacityEnforcingLinkedHashMap<K, V>( |
| builder.getInitialCapacity(), |
| 0.75f, |
| (builder.maximumSize != UNSET_INT), |
| builder.maximumSize, |
| statsCounter, |
| removalListener); |
| |
| this.ticker = firstNonNull(builder.ticker, Ticker.systemTicker()); |
| } |
| |
| @Override |
| public int size() { |
| return cachingHashMap.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return cachingHashMap.isEmpty(); |
| } |
| |
| @Override |
| public V get(Object key) { |
| checkNotNull(key); |
| Timestamped<V> value = cachingHashMap.get(key); |
| |
| if (value == null) { |
| statsCounter.recordMisses(1); |
| return null; |
| } else if (!isExpired(value)) { |
| statsCounter.recordHits(1); |
| value.updateTimestamp(); |
| return value.getValue(); |
| } else { |
| statsCounter.recordEviction(); |
| statsCounter.recordMisses(1); |
| alertListenerIfPresent(key, value.getValue(), RemovalCause.EXPIRED); |
| cachingHashMap.remove(key); |
| return null; |
| } |
| } |
| |
| @Override |
| public V put(K key, V value) { |
| checkNotNull(key); |
| checkNotNull(value); |
| Timestamped<V> oldValue = cachingHashMap.put(key, new Timestamped<V>(value, ticker)); |
| if (oldValue == null) { |
| return null; |
| } |
| alertListenerIfPresent(key, oldValue.getValue(), RemovalCause.REPLACED); |
| return oldValue.getValue(); |
| } |
| |
| @Override |
| public V remove(Object key) { |
| Timestamped<V> stamped = cachingHashMap.remove(key); |
| if (stamped != null) { |
| V value = stamped.getValue(); |
| |
| if (!isExpired(stamped)) { |
| alertListenerIfPresent(key, value, RemovalCause.EXPLICIT); |
| return value; |
| } |
| |
| alertListenerIfPresent(key, value, RemovalCause.EXPIRED); |
| } |
| return null; |
| } |
| |
| @Override |
| public void putAll(Map<? extends K, ? extends V> m) { |
| for (Entry<? extends K, ? extends V> entry : m.entrySet()) { |
| put(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| @Override |
| public void clear() { |
| if (removalListener != null) { |
| for (Entry<K, Timestamped<V>> entry : cachingHashMap.entrySet()) { |
| alertListenerIfPresent(entry.getKey(), entry.getValue().getValue(), RemovalCause.EXPLICIT); |
| } |
| } |
| cachingHashMap.clear(); |
| } |
| |
| @Override |
| public V putIfAbsent(K key, V value) { |
| V currentValue = get(key); |
| if (currentValue != null) { |
| return currentValue; |
| } |
| return put(key, value); |
| } |
| |
| @Override |
| public boolean remove(Object key, Object value) { |
| if (value.equals(get(key))) { |
| alertListenerIfPresent(key, value, RemovalCause.EXPLICIT); |
| remove(key); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean replace(K key, V oldValue, V newValue) { |
| if (oldValue.equals(get(key))) { |
| alertListenerIfPresent(key, oldValue, RemovalCause.REPLACED); |
| put(key, newValue); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public V replace(K key, V value) { |
| V currentValue = get(key); |
| if (currentValue != null) { |
| alertListenerIfPresent(key, currentValue, RemovalCause.REPLACED); |
| return put(key, value); |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean containsKey(Object key) { |
| return cachingHashMap.containsKey(key) && !isExpired(cachingHashMap.get(key)); |
| } |
| |
| @Override |
| public boolean containsValue(Object value) { |
| for (Timestamped<V> val : cachingHashMap.values()) { |
| if (val.getValue().equals(value)) { |
| if (!isExpired(val)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean isExpired(Timestamped<V> stamped) { |
| if ((expireAfterAccess == UNSET_INT) && (expireAfterWrite == UNSET_INT)) { |
| return false; |
| } |
| |
| boolean expireWrite = (stamped.getWriteTimestamp() + expireAfterWrite <= currentTimeNanos()); |
| boolean expireAccess = (stamped.getAccessTimestamp() + expireAfterAccess <= currentTimeNanos()); |
| |
| if (expireAfterAccess == UNSET_INT) { |
| return expireWrite; |
| } |
| if (expireAfterWrite == UNSET_INT) { |
| return expireAccess; |
| } |
| |
| return expireWrite || expireAccess; |
| } |
| |
| @SuppressWarnings("GoodTime") |
| private long currentTimeNanos() { |
| return ticker.read(); |
| } |
| |
| private void alertListenerIfPresent(Object key, Object value, RemovalCause cause) { |
| if (removalListener != null) { |
| removalListener.onRemoval(RemovalNotification.create(key, value, cause)); |
| } |
| } |
| |
| @SuppressWarnings("GoodTime") // timestamps as numeric primitives |
| private V load(Object key) throws ExecutionException { |
| long startTime = ticker.read(); |
| V calculatedValue; |
| try { |
| /* |
| * This cast isn't safe, but we can rely on the fact that K is almost always passed to |
| * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the |
| * case. |
| * |
| * The alternative is to add an overloaded method, but the chances of a user calling get() |
| * instead of the new API and the risks inherent in adding a new API outweigh this little |
| * hole. |
| */ |
| K castKey = (K) key; |
| calculatedValue = loader.load(castKey); |
| put(castKey, calculatedValue); |
| } catch (RuntimeException e) { |
| statsCounter.recordLoadException(ticker.read() - startTime); |
| throw new UncheckedExecutionException(e); |
| } catch (Exception e) { |
| statsCounter.recordLoadException(ticker.read() - startTime); |
| throw new ExecutionException(e); |
| } catch (Error e) { |
| statsCounter.recordLoadException(ticker.read() - startTime); |
| throw new ExecutionError(e); |
| } |
| |
| if (calculatedValue == null) { |
| String message = loader + " returned null for key " + key + "."; |
| throw new CacheLoader.InvalidCacheLoadException(message); |
| } |
| statsCounter.recordLoadSuccess(ticker.read() - startTime); |
| return calculatedValue; |
| } |
| |
| private V getIfPresent(Object key) { |
| checkNotNull(key); |
| Timestamped<V> value = cachingHashMap.get(key); |
| |
| if (value == null) { |
| return null; |
| } else if (!isExpired(value)) { |
| value.updateTimestamp(); |
| return value.getValue(); |
| } else { |
| alertListenerIfPresent(key, value.getValue(), RemovalCause.EXPIRED); |
| cachingHashMap.remove(key); |
| return null; |
| } |
| } |
| |
| private V getOrLoad(K key) throws ExecutionException { |
| V value = get(key); |
| if (value != null) { |
| return value; |
| } |
| return load(key); |
| } |
| |
| @SuppressWarnings("GoodTime") // timestamps as numeric primitives |
| private static class Timestamped<V> { |
| private final V value; |
| private final Ticker ticker; |
| private long writeTimestamp; |
| private long accessTimestamp; |
| |
| public Timestamped(V value, Ticker ticker) { |
| this.value = checkNotNull(value); |
| this.ticker = checkNotNull(ticker); |
| this.writeTimestamp = ticker.read(); |
| this.accessTimestamp = this.writeTimestamp; |
| } |
| |
| public V getValue() { |
| return value; |
| } |
| |
| public void updateTimestamp() { |
| accessTimestamp = ticker.read(); |
| } |
| |
| public long getAccessTimestamp() { |
| return accessTimestamp; |
| } |
| |
| public long getWriteTimestamp() { |
| return writeTimestamp; |
| } |
| |
| public boolean equals(Object o) { |
| return value.equals(o); |
| } |
| |
| public int hashCode() { |
| return value.hashCode(); |
| } |
| } |
| |
| /** |
| * LocalManualCache is a wrapper around LocalCache for a cache without loading. |
| * |
| * @param <K> the base key type |
| * @param <V> the base value type |
| */ |
| public static class LocalManualCache<K, V> extends AbstractCache<K, V> { |
| final LocalCache<K, V> localCache; |
| |
| LocalManualCache(CacheBuilder<? super K, ? super V> builder) { |
| this(builder, null); |
| } |
| |
| protected LocalManualCache( |
| CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { |
| this.localCache = new LocalCache<K, V>(builder, loader); |
| } |
| |
| // Cache methods |
| |
| @Override |
| public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException { |
| V value = localCache.get(key); |
| if (value != null) { |
| return value; |
| } |
| |
| try { |
| V newValue = valueLoader.call(); |
| localCache.put(key, newValue); |
| return newValue; |
| } catch (Exception e) { |
| throw new ExecutionException(e); |
| } |
| } |
| |
| @Override |
| public @Nullable V getIfPresent(Object key) { |
| return localCache.getIfPresent(key); |
| } |
| |
| @Override |
| public void put(K key, V value) { |
| localCache.put(key, value); |
| } |
| |
| @Override |
| public void invalidate(Object key) { |
| checkNotNull(key); |
| localCache.remove(key); |
| } |
| |
| @Override |
| public void invalidateAll() { |
| localCache.clear(); |
| } |
| |
| @Override |
| public long size() { |
| return localCache.size(); |
| } |
| |
| @Override |
| public ConcurrentMap<K, V> asMap() { |
| return localCache; |
| } |
| } |
| |
| /** |
| * LocalLoadingCache is a wrapper around LocalCache for a cache with loading. |
| * |
| * @param <K> the base key type |
| * @param <V> the base value type |
| */ |
| public static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> |
| implements LoadingCache<K, V> { |
| |
| LocalLoadingCache( |
| CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { |
| super(builder, checkNotNull(loader)); |
| } |
| |
| // Cache methods |
| |
| @Override |
| public V get(K key) throws ExecutionException { |
| return localCache.getOrLoad(key); |
| } |
| |
| @Override |
| public V getUnchecked(K key) { |
| try { |
| return get(key); |
| } catch (ExecutionException e) { |
| throw new UncheckedExecutionException(e.getCause()); |
| } |
| } |
| |
| @Override |
| public final V apply(K key) { |
| return getUnchecked(key); |
| } |
| |
| @Override |
| public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { |
| Map<K, V> map = new HashMap<K, V>(); |
| for (K key : keys) { |
| map.put(key, localCache.getOrLoad(key)); |
| } |
| return ImmutableMap.copyOf(map); |
| } |
| |
| @Override |
| public void refresh(K key) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| /** |
| * LinkedHashMap that enforces it's maximum size and logs events in a StatsCounter object and an |
| * optional RemovalListener. |
| * |
| * @param <K> the base key type |
| * @param <V> the base value type |
| */ |
| private class CapacityEnforcingLinkedHashMap<K, V> extends LinkedHashMap<K, Timestamped<V>> { |
| |
| private final StatsCounter statsCounter; |
| private final RemovalListener removalListener; |
| private final long maximumSize; |
| |
| public CapacityEnforcingLinkedHashMap( |
| int initialCapacity, |
| float loadFactor, |
| boolean accessOrder, |
| long maximumSize, |
| StatsCounter statsCounter, |
| @Nullable RemovalListener removalListener) { |
| super(initialCapacity, loadFactor, accessOrder); |
| this.maximumSize = maximumSize; |
| this.statsCounter = statsCounter; |
| this.removalListener = removalListener; |
| } |
| |
| @Override |
| protected boolean removeEldestEntry(Entry<K, Timestamped<V>> ignored) { |
| boolean removal = (maximumSize == UNSET_INT) ? false : (size() > maximumSize); |
| if ((removalListener != null) && removal) { |
| removalListener.onRemoval( |
| RemovalNotification.create( |
| ignored.getKey(), ignored.getValue().getValue(), RemovalCause.SIZE)); |
| } |
| statsCounter.recordEviction(); |
| return removal; |
| } |
| } |
| |
| /** |
| * Any updates to LocalCache.Strength used in CacheBuilder need to be matched in this class for |
| * compilation purposes. |
| */ |
| enum Strength { |
| /* |
| * TODO(kevinb): If we strongly reference the value and aren't loading, we needn't wrap the |
| * value. This could save ~8 bytes per entry. |
| */ |
| |
| STRONG { |
| @Override |
| Equivalence<Object> defaultEquivalence() { |
| return Equivalence.equals(); |
| } |
| }, |
| |
| SOFT { |
| @Override |
| Equivalence<Object> defaultEquivalence() { |
| return Equivalence.identity(); |
| } |
| }, |
| |
| WEAK { |
| @Override |
| Equivalence<Object> defaultEquivalence() { |
| return Equivalence.identity(); |
| } |
| }; |
| |
| abstract Equivalence<Object> defaultEquivalence(); |
| } |
| |
| /** |
| * Implementation for the EntryIterator, which is used to build Key and Value iterators. |
| * |
| * <p>Expiration is only checked on hasNext(), so as to ensure that a next() call never returns |
| * null when hasNext() has already been called. |
| */ |
| class EntryIterator implements Iterator<Entry<K, V>> { |
| Iterator<Entry<K, Timestamped<V>>> iterator; |
| Entry<K, Timestamped<V>> lastEntry; |
| Entry<K, Timestamped<V>> nextEntry; |
| |
| EntryIterator() { |
| this.iterator = LocalCache.this.cachingHashMap.entrySet().iterator(); |
| } |
| |
| @Override |
| public Entry<K, V> next() { |
| if (nextEntry == null) { |
| hasNext(); |
| |
| if (nextEntry == null) { |
| throw new NoSuchElementException(); |
| } |
| } |
| |
| lastEntry = nextEntry; |
| nextEntry = null; |
| return new WriteThroughEntry(lastEntry.getKey(), lastEntry.getValue().getValue()); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| if (nextEntry == null) { |
| while (iterator.hasNext()) { |
| Entry<K, Timestamped<V>> next = iterator.next(); |
| if (!isExpired(next.getValue())) { |
| nextEntry = next; |
| return true; |
| } |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public void remove() { |
| checkState(lastEntry != null); |
| LocalCache.this.remove(lastEntry.getKey(), lastEntry.getValue()); |
| lastEntry = null; |
| } |
| } |
| |
| /** KeyIterator build on top of EntryIterator. */ |
| final class KeyIterator implements Iterator<K> { |
| private EntryIterator iterator; |
| |
| KeyIterator() { |
| iterator = new EntryIterator(); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return iterator.hasNext(); |
| } |
| |
| @Override |
| public K next() { |
| return iterator.next().getKey(); |
| } |
| |
| @Override |
| public void remove() { |
| iterator.remove(); |
| } |
| } |
| |
| /** ValueIterator build on top of EntryIterator. */ |
| final class ValueIterator implements Iterator<V> { |
| private EntryIterator iterator; |
| |
| ValueIterator() { |
| iterator = new EntryIterator(); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return iterator.hasNext(); |
| } |
| |
| @Override |
| public V next() { |
| return iterator.next().getValue(); |
| } |
| |
| @Override |
| public void remove() { |
| iterator.remove(); |
| } |
| } |
| |
| Set<K> keySet; |
| |
| @Override |
| public Set<K> keySet() { |
| // does not impact recency ordering |
| Set<K> ks = keySet; |
| return (ks != null) ? ks : (keySet = new KeySet(this)); |
| } |
| |
| Collection<V> values; |
| |
| @Override |
| public Collection<V> values() { |
| // does not impact recency ordering |
| Collection<V> vs = values; |
| return (vs != null) ? vs : (values = new Values(this)); |
| } |
| |
| Set<Entry<K, V>> entrySet; |
| |
| @Override |
| public Set<Entry<K, V>> entrySet() { |
| // does not impact recency ordering |
| Set<Entry<K, V>> es = entrySet; |
| return (es != null) ? es : (entrySet = new EntrySet(this)); |
| } |
| |
| /** |
| * Custom Entry class used by EntryIterator.next(), that relays setValue changes to the underlying |
| * map. |
| */ |
| private final class WriteThroughEntry implements Entry<K, V> { |
| final K key; |
| V value; |
| |
| WriteThroughEntry(K key, V value) { |
| this.key = checkNotNull(key); |
| this.value = checkNotNull(value); |
| } |
| |
| @Override |
| public K getKey() { |
| return key; |
| } |
| |
| @Override |
| public V getValue() { |
| return value; |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object object) { |
| // Cannot use key and value equivalence |
| if (object instanceof Entry) { |
| Entry<?, ?> that = (Entry<?, ?>) object; |
| return key.equals(that.getKey()) && value.equals(that.getValue()); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| // Cannot use key and value equivalence |
| return key.hashCode() ^ value.hashCode(); |
| } |
| |
| @Override |
| public V setValue(V newValue) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public String toString() { |
| return getKey() + "=" + getValue(); |
| } |
| } |
| |
| // TODO(fry): Separate logic for consistency between emul and nonemul implementation. |
| // TODO(fry): Look into Maps.KeySet and Maps.Values, which can ideally be reused here but are |
| // currently only package visible. |
| abstract class AbstractCacheSet<T> extends AbstractSet<T> { |
| final ConcurrentMap<?, ?> map; |
| |
| AbstractCacheSet(ConcurrentMap<?, ?> map) { |
| this.map = map; |
| } |
| |
| @Override |
| public int size() { |
| return map.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return map.isEmpty(); |
| } |
| |
| @Override |
| public void clear() { |
| map.clear(); |
| } |
| } |
| |
| private final class KeySet extends AbstractCacheSet<K> { |
| |
| KeySet(ConcurrentMap<?, ?> map) { |
| super(map); |
| } |
| |
| @Override |
| public Iterator<K> iterator() { |
| return new KeyIterator(); |
| } |
| |
| @Override |
| public boolean contains(Object o) { |
| return map.containsKey(o); |
| } |
| |
| @Override |
| public boolean remove(Object o) { |
| return map.remove(o) != null; |
| } |
| } |
| |
| private final class Values extends AbstractCollection<V> { |
| final ConcurrentMap<?, ?> map; |
| |
| Values(ConcurrentMap<?, ?> map) { |
| this.map = map; |
| } |
| |
| @Override |
| public Iterator<V> iterator() { |
| return new ValueIterator(); |
| } |
| |
| @Override |
| public boolean contains(Object o) { |
| return map.containsValue(o); |
| } |
| |
| @Override |
| public int size() { |
| return map.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return map.isEmpty(); |
| } |
| |
| @Override |
| public void clear() { |
| map.clear(); |
| } |
| } |
| |
| private final class EntrySet extends AbstractCacheSet<Entry<K, V>> { |
| |
| EntrySet(ConcurrentMap<?, ?> map) { |
| super(map); |
| } |
| |
| @Override |
| public Iterator<Entry<K, V>> iterator() { |
| return new EntryIterator(); |
| } |
| |
| @Override |
| public boolean contains(Object o) { |
| if (!(o instanceof Entry)) { |
| return false; |
| } |
| Entry<?, ?> e = (Entry<?, ?>) o; |
| Object key = e.getKey(); |
| if (key == null) { |
| return false; |
| } |
| V v = LocalCache.this.get(key); |
| |
| return (v != null) && e.getValue().equals(v); |
| } |
| |
| @Override |
| public boolean remove(Object o) { |
| if (!(o instanceof Entry)) { |
| return false; |
| } |
| Entry<?, ?> e = (Entry<?, ?>) o; |
| Object key = e.getKey(); |
| return (key != null) && LocalCache.this.remove(key, e.getValue()); |
| } |
| } |
| } |