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
17package com.google.common.cache;
18
19import static com.google.common.base.Preconditions.checkArgument;
20import static com.google.common.base.Preconditions.checkNotNull;
21import static com.google.common.base.Preconditions.checkState;
22
23import com.google.common.base.Function;
24import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
25import com.google.common.collect.ImmutableMap;
26import com.google.common.util.concurrent.ExecutionError;
27import com.google.common.util.concurrent.UncheckedExecutionException;
28import com.google.gwt.user.client.Timer;
29
30import java.util.LinkedHashMap;
31import java.util.Map;
32import java.util.concurrent.ConcurrentHashMap;
33import java.util.concurrent.ConcurrentMap;
34import java.util.concurrent.ExecutionException;
35import java.util.concurrent.TimeUnit;
36
37import 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
45public 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