Home | History | Annotate | Download | only in cache
      1 /*
      2  * Copyright (C) 2011 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.cache;
     18 
     19 import static com.google.common.base.Preconditions.checkArgument;
     20 import static com.google.common.base.Preconditions.checkNotNull;
     21 import static com.google.common.base.Preconditions.checkState;
     22 
     23 import com.google.common.base.Function;
     24 import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
     25 import com.google.common.collect.ImmutableMap;
     26 import com.google.common.util.concurrent.ExecutionError;
     27 import com.google.common.util.concurrent.UncheckedExecutionException;
     28 import com.google.gwt.user.client.Timer;
     29 
     30 import java.util.LinkedHashMap;
     31 import java.util.Map;
     32 import java.util.concurrent.ConcurrentHashMap;
     33 import java.util.concurrent.ConcurrentMap;
     34 import java.util.concurrent.ExecutionException;
     35 import java.util.concurrent.TimeUnit;
     36 
     37 import javax.annotation.Nullable;
     38 
     39 /**
     40  * CacheBuilder emulation.
     41  *
     42  * @author Charles Fry
     43  */
     44 // TODO(fry): eventually we should emmulate LocalCache instead of CacheBuilder
     45 public class CacheBuilder<K, V> {
     46   private static final int UNSET_INT = -1;
     47   private static final int DEFAULT_INITIAL_CAPACITY = 16;
     48   private static final int DEFAULT_EXPIRATION_NANOS = 0;
     49 
     50   private int initialCapacity = -1;
     51   private int concurrencyLevel = -1;
     52   private long expirationMillis = -1;
     53   private int maximumSize = -1;
     54 
     55   CacheBuilder() {}
     56 
     57   public static CacheBuilder<Object, Object> newBuilder() {
     58     return new CacheBuilder<Object, Object>();
     59   }
     60 
     61   public CacheBuilder<K, V> initialCapacity(int initialCapacity) {
     62     checkState(this.initialCapacity == UNSET_INT, "initial capacity was already set to %s",
     63         this.initialCapacity);
     64     checkArgument(initialCapacity >= 0);
     65     this.initialCapacity = initialCapacity;
     66     return this;
     67   }
     68 
     69   private int getInitialCapacity() {
     70     return (initialCapacity == UNSET_INT) ? DEFAULT_INITIAL_CAPACITY : initialCapacity;
     71   }
     72 
     73   public CacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) {
     74     checkState(this.concurrencyLevel == UNSET_INT, "concurrency level was already set to %s",
     75         this.concurrencyLevel);
     76     checkArgument(concurrencyLevel > 0);
     77     // GWT technically only supports concurrencyLevel == 1, but we silently
     78     // ignore other positive values.
     79     this.concurrencyLevel = concurrencyLevel;
     80     return this;
     81   }
     82 
     83   public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
     84     checkState(expirationMillis == UNSET_INT, "expireAfterWrite was already set to %s ms",
     85         expirationMillis);
     86     checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
     87     this.expirationMillis = unit.toMillis(duration);
     88     return this;
     89   }
     90 
     91   private long getExpirationMillis() {
     92     return (expirationMillis == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expirationMillis;
     93   }
     94 
     95   public CacheBuilder<K, V> maximumSize(int maximumSize) {
     96     if (this.maximumSize != -1) {
     97       throw new IllegalStateException("maximum size of " + maximumSize + " was already set");
     98     }
     99     if (maximumSize < 0) {
    100       throw new IllegalArgumentException("invalid maximum size: " + maximumSize);
    101     }
    102     this.maximumSize = maximumSize;
    103     return this;
    104   }
    105 
    106   public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    107     return new LocalManualCache<K1, V1>(this);
    108   }
    109 
    110   public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
    111       CacheLoader<? super K1, V1> loader) {
    112     return new LocalLoadingCache<K1, V1>(this, loader);
    113   }
    114 
    115   private static class LocalManualCache<K, V>
    116       extends AbstractCache<K, V> implements Function<K, V> {
    117     final LocalCache<K, V> localCache;
    118 
    119     LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
    120       this(builder, null);
    121     }
    122 
    123     protected LocalManualCache(CacheBuilder<? super K, ? super V> builder,
    124         CacheLoader<? super K, V> loader) {
    125       this.localCache = new LocalCache<K, V>(builder, loader);
    126     }
    127 
    128     // Cache methods
    129 
    130     @Override
    131     public V get(K key) throws ExecutionException {
    132       return localCache.getOrLoad(key);
    133     }
    134 
    135     @Override
    136     public V getUnchecked(K key) {
    137       try {
    138         return get(key);
    139       } catch (ExecutionException e) {
    140         throw new UncheckedExecutionException(e.getCause());
    141       }
    142     }
    143 
    144     @Override
    145     public final V apply(K key) {
    146       return getUnchecked(key);
    147     }
    148 
    149     @Override
    150     @Nullable
    151     public V getIfPresent(K key) {
    152       return localCache.get(key);
    153     }
    154 
    155     @Override
    156     public void put(K key, V value) {
    157       localCache.put(key, value);
    158     }
    159 
    160     @Override
    161     public void invalidate(Object key) {
    162       checkNotNull(key);
    163       localCache.remove(key);
    164     }
    165 
    166     @Override
    167     public void invalidateAll() {
    168       localCache.clear();
    169     }
    170 
    171     @Override
    172     public long size() {
    173       return localCache.size();
    174     }
    175 
    176     @Override
    177     public ConcurrentMap<K, V> asMap() {
    178       return localCache;
    179     }
    180   }
    181 
    182   private static class LocalLoadingCache<K, V>
    183       extends LocalManualCache<K, V> implements LoadingCache<K, V> {
    184 
    185     LocalLoadingCache(CacheBuilder<? super K, ? super V> builder,
    186         CacheLoader<? super K, V> loader) {
    187       super(builder, checkNotNull(loader));
    188     }
    189 
    190     // Cache methods
    191 
    192     @Override
    193     public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
    194       throw new UnsupportedOperationException();
    195     }
    196 
    197     @Override
    198     public void refresh(K key) {
    199       throw new UnsupportedOperationException();
    200     }
    201   }
    202 
    203   // TODO(fry,user): ConcurrentHashMap never throws a CME when mutating the map during iteration, but
    204   // this implementation (based on a LHM) does. This will all be replaced soon anyways, so leaving
    205   // it as is for now.
    206   private static class LocalCache<K, V> extends LinkedHashMap<K, V>
    207       implements ConcurrentMap<K, V> {
    208     private final CacheLoader<? super K, V> loader;
    209     private final long expirationMillis;
    210     private final int maximumSize;
    211 
    212     LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
    213       super(builder.getInitialCapacity(), 0.75f, (builder.maximumSize != UNSET_INT));
    214       this.loader = loader;
    215       this.expirationMillis = builder.getExpirationMillis();
    216       this.maximumSize = builder.maximumSize;
    217     }
    218 
    219     @Override
    220     public V put(K key, V value) {
    221       V result = super.put(key, value);
    222       if (expirationMillis > 0) {
    223         scheduleRemoval(key, value);
    224       }
    225       return result;
    226     }
    227 
    228     @Override
    229     protected boolean removeEldestEntry(Map.Entry<K, V> ignored) {
    230       return (maximumSize == -1) ? false : size() > maximumSize;
    231     }
    232 
    233     @Override
    234     public V putIfAbsent(K key, V value) {
    235       if (!containsKey(key)) {
    236         return put(key, value);
    237       } else {
    238         return get(key);
    239       }
    240     }
    241 
    242     @Override
    243     public boolean remove(Object key, Object value) {
    244       if (containsKey(key) && get(key).equals(value)) {
    245         remove(key);
    246         return true;
    247       }
    248       return false;
    249     }
    250 
    251     @Override
    252     public boolean replace(K key, V oldValue, V newValue) {
    253       if (containsKey(key) && get(key).equals(oldValue)) {
    254         put(key, newValue);
    255         return true;
    256       }
    257       return false;
    258     }
    259 
    260     @Override
    261     public V replace(K key, V value) {
    262       return containsKey(key) ? put(key, value) : null;
    263     }
    264 
    265     private void scheduleRemoval(final K key, final V value) {
    266       /*
    267        * TODO: Keep weak reference to map, too. Build a priority queue out of the entries themselves
    268        * instead of creating a task per entry. Then, we could have one recurring task per map (which
    269        * would clean the entire map and then reschedule itself depending upon when the next
    270        * expiration comes). We also want to avoid removing an entry prematurely if the entry was set
    271        * to the same value again.
    272        */
    273       Timer timer = new Timer() {
    274         @Override
    275         public void run() {
    276           remove(key, value);
    277         }
    278       };
    279       timer.schedule((int) expirationMillis);
    280     }
    281 
    282     public V getOrLoad(Object k) throws ExecutionException {
    283       // from CustomConcurrentHashMap
    284       V result = super.get(k);
    285       if (result == null) {
    286         /*
    287          * This cast isn't safe, but we can rely on the fact that K is almost always passed to
    288          * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the
    289          * case.
    290          *
    291          * The alternative is to add an overloaded method, but the chances of a user calling get()
    292          * instead of the new API and the risks inherent in adding a new API outweigh this little
    293          * hole.
    294          */
    295         @SuppressWarnings("unchecked")
    296         K key = (K) k;
    297         result = compute(key);
    298       }
    299       return result;
    300     }
    301 
    302     private V compute(K key) throws ExecutionException {
    303       V value;
    304       try {
    305         value = loader.load(key);
    306       } catch (RuntimeException e) {
    307         throw new UncheckedExecutionException(e);
    308       } catch (Exception e) {
    309         throw new ExecutionException(e);
    310       } catch (Error e) {
    311         throw new ExecutionError(e);
    312       }
    313 
    314       if (value == null) {
    315         String message = loader + " returned null for key " + key + ".";
    316         throw new InvalidCacheLoadException(message);
    317       }
    318       put(key, value);
    319       return value;
    320     }
    321   }
    322 
    323 }
    324