• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
CacheBuilder()55   CacheBuilder() {}
56 
newBuilder()57   public static CacheBuilder<Object, Object> newBuilder() {
58     return new CacheBuilder<Object, Object>();
59   }
60 
initialCapacity(int initialCapacity)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 
getInitialCapacity()69   private int getInitialCapacity() {
70     return (initialCapacity == UNSET_INT) ? DEFAULT_INITIAL_CAPACITY : initialCapacity;
71   }
72 
concurrencyLevel(int concurrencyLevel)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 
expireAfterWrite(long duration, TimeUnit unit)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 
getExpirationMillis()91   private long getExpirationMillis() {
92     return (expirationMillis == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expirationMillis;
93   }
94 
maximumSize(int maximumSize)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 
build()106   public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
107     return new LocalManualCache<K1, V1>(this);
108   }
109 
build( CacheLoader<? super K1, V1> loader)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 
LocalManualCache(CacheBuilder<? super K, ? super V> builder)119     LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
120       this(builder, null);
121     }
122 
LocalManualCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)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
get(K key)131     public V get(K key) throws ExecutionException {
132       return localCache.getOrLoad(key);
133     }
134 
135     @Override
getUnchecked(K key)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
apply(K key)145     public final V apply(K key) {
146       return getUnchecked(key);
147     }
148 
149     @Override
150     @Nullable
getIfPresent(K key)151     public V getIfPresent(K key) {
152       return localCache.get(key);
153     }
154 
155     @Override
put(K key, V value)156     public void put(K key, V value) {
157       localCache.put(key, value);
158     }
159 
160     @Override
invalidate(Object key)161     public void invalidate(Object key) {
162       checkNotNull(key);
163       localCache.remove(key);
164     }
165 
166     @Override
invalidateAll()167     public void invalidateAll() {
168       localCache.clear();
169     }
170 
171     @Override
size()172     public long size() {
173       return localCache.size();
174     }
175 
176     @Override
asMap()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 
LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)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
getAll(Iterable<? extends K> keys)193     public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
194       throw new UnsupportedOperationException();
195     }
196 
197     @Override
refresh(K key)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 
LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)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
put(K key, V value)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
removeEldestEntry(Map.Entry<K, V> ignored)229     protected boolean removeEldestEntry(Map.Entry<K, V> ignored) {
230       return (maximumSize == -1) ? false : size() > maximumSize;
231     }
232 
233     @Override
putIfAbsent(K key, V value)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
remove(Object key, Object value)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
replace(K key, V oldValue, V newValue)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
replace(K key, V value)261     public V replace(K key, V value) {
262       return containsKey(key) ? put(key, value) : null;
263     }
264 
scheduleRemoval(final K key, final V value)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 
getOrLoad(Object k)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 
compute(K key)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