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