package com.comphenix.protocol.utility; import java.lang.reflect.Method; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.base.Ticker; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.RemovalListener; /** * Represents a Guava CacheBuilder that is compatible with both Guava 10 and 13. * * @author Kristian */ public class SafeCacheBuilder { private CacheBuilder builder; private static Method BUILD_METHOD; private static Method AS_MAP_METHOD; @SuppressWarnings("unchecked") private SafeCacheBuilder() { builder = (CacheBuilder) CacheBuilder.newBuilder(); } /** * Construct a new safe cache builder. * @param Key type * @param Value type * * @return A new cache builder. */ public static SafeCacheBuilder newBuilder() { return new SafeCacheBuilder(); } /** * Guides the allowed concurrency among update operations. Used as a hint * for internal sizing. The table is internally partitioned to try to permit * the indicated number of concurrent updates without contention. Because * assignment of entries to these partitions is not necessarily uniform, the * actual concurrency observed may vary. Ideally, you should choose a value * to accommodate as many threads as will ever concurrently modify the * table. Using a significantly higher value than you need can waste space * and time, and a significantly lower value can lead to thread contention. * But overestimates and underestimates within an order of magnitude do not * usually have much noticeable impact. A value of one permits only one * thread to modify the cache at a time, but since read operations can * proceed concurrently, this still yields higher concurrency than full * synchronization. Defaults to 4. * *

* Note:The default may change in the future. If you care about this * value, you should always choose it explicitly. * * @param concurrencyLevel New concurrency level * @return This for chaining * * @throws IllegalArgumentException if {@code concurrencyLevel} is * nonpositive * @throws IllegalStateException if a concurrency level was already set */ public SafeCacheBuilder concurrencyLevel(int concurrencyLevel) { builder.concurrencyLevel(concurrencyLevel); return this; } /** * Specifies that each entry should be automatically removed from the cache * once a fixed duration has elapsed after the entry's creation, or last * access. Access time is reset by {@link com.google.common.cache.Cache#get Cache.get()}, * but not by operations on the view returned by * {@link com.google.common.cache.Cache#asMap() Cache.asMap()}. * *

* When {@code duration} is zero, elements will be evicted immediately after * being loaded into the cache. This has the same effect as invoking * {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, * or to disable caching temporarily without a code change. * *

* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be * visible to read or write operations. Expired entries are currently * cleaned up during write operations, or during occasional read operations * in the absense of writes; though this behavior may change in the future. * * @param duration the length of time after an entry is last accessed that * it should be automatically removed * @param unit the unit that {@code duration} is expressed in * @return This for chaining * * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to idle or time to live was * already set */ public SafeCacheBuilder expireAfterAccess(long duration, TimeUnit unit) { builder.expireAfterAccess(duration, unit); return this; } /** * Specifies that each entry should be automatically removed from the cache * once a fixed duration has elapsed after the entry's creation, or the most * recent replacement of its value. * *

* When {@code duration} is zero, elements will be evicted immediately after * being loaded into the cache. This has the same effect as invoking * {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, * or to disable caching temporarily without a code change. * *

* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be * visible to read or write operations. Expired entries are currently * cleaned up during write operations, or during occasional read operations * in the absense of writes; though this behavior may change in the future. * * @param duration the length of time after an entry is created that it * should be automatically removed * @param unit the unit that {@code duration} is expressed in * @return This for chaining * * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to live or time to idle was * already set */ public SafeCacheBuilder expireAfterWrite(long duration, TimeUnit unit) { builder.expireAfterWrite(duration, unit); return this; } /** * Sets the minimum total size for the internal hash tables. For example, if * the initial capacity is {@code 60}, and the concurrency level is * {@code 8}, then eight segments are created, each having a hash table of * size eight. Providing a large enough estimate at construction time avoids * the need for expensive resizing operations later, but setting this value * unnecessarily high wastes memory. * * @param initialCapacity - initial capacity * @return This for chaining * * @throws IllegalArgumentException if {@code initialCapacity} is negative * @throws IllegalStateException if an initial capacity was already set */ public SafeCacheBuilder initialCapacity(int initialCapacity) { builder.initialCapacity(initialCapacity); return this; } /** * Specifies the maximum number of entries the cache may contain. Note that * the cache may evict an entry before this limit is exceeded. As the * cache size grows close to the maximum, the cache evicts entries that are * less likely to be used again. For example, the cache may evict an entry * because it hasn't been used recently or very often. * *

* When {@code size} is zero, elements will be evicted immediately after * being loaded into the cache. This has the same effect as invoking * {@link #expireAfterWrite expireAfterWrite}{@code (0, unit)} or * {@link #expireAfterAccess expireAfterAccess}{@code (0, * unit)}. It can be useful in testing, or to disable caching temporarily * without a code change. * * @param size the maximum size of the cache * @return This for chaining * * @throws IllegalArgumentException if {@code size} is negative * @throws IllegalStateException if a maximum size was already set */ public SafeCacheBuilder maximumSize(int size) { builder.maximumSize(size); return this; } /** * Specifies a listener instance, which all caches built using this * {@code CacheBuilder} will notify each time an entry is removed from the * cache by any means. * *

* Each cache built by this {@code CacheBuilder} after this method is called * invokes the supplied listener after removing an element for any reason * (see removal causes in {@link com.google.common.cache.RemovalCause RemovalCause}). It will invoke the listener * during invocations of any of that cache's public methods (even read-only * methods). * *

* Important note: Instead of returning this as a * {@code CacheBuilder} instance, this method returns * {@code CacheBuilder}. From this point on, either the original * reference or the returned reference may be used to complete configuration * and build the cache, but only the "generic" one is type-safe. That is, it * will properly prevent you from building caches whose key or value types * are incompatible with the types accepted by the listener already * provided; the {@code CacheBuilder} type cannot do this. For best results, * simply use the standard method-chaining idiom, as illustrated in the * documentation at top, configuring a {@code CacheBuilder} and building * your {@link com.google.common.cache.Cache Cache} all in a single statement. * *

* Warning: if you ignore the above advice, and use this * {@code CacheBuilder} to build a cache whose key or value type is * incompatible with the listener, you will likely experience a * {@link ClassCastException} at some undefined point in the future. * * @param Key type * @param Value type * @param listener - removal listener * @return This for chaining * * @throws IllegalStateException if a removal listener was already set */ @SuppressWarnings("unchecked") public SafeCacheBuilder removalListener(RemovalListener listener) { builder.removalListener(listener); return (SafeCacheBuilder) this; } /** * Specifies a nanosecond-precision time source for use in determining when * entries should be expired. By default, {@link System#nanoTime} is used. * *

* The primary intent of this method is to facilitate testing of caches * which have been configured with {@link #expireAfterWrite} or * {@link #expireAfterAccess}. * * @param ticker - ticker * @return This for chaining * * @throws IllegalStateException if a ticker was already set */ public SafeCacheBuilder ticker(Ticker ticker) { builder.ticker(ticker); return this; } /** * Specifies that each value (not key) stored in the cache should be wrapped * in a {@link java.lang.ref.SoftReference SoftReference} (by default, strong references are used). * Softly-referenced objects will be garbage-collected in a globally * least-recently-used manner, in response to memory demand. * *

* Warning: in most circumstances it is better to set a per-cache * {@linkplain #maximumSize maximum size} instead of using soft references. * You should only use this method if you are well familiar with the * practical consequences of soft references. * *

* Note: when this method is used, the resulting cache will use * identity ({@code ==}) comparison to determine equality of values. * * @return This for chaining * * @throws IllegalStateException if the value strength was already set */ public SafeCacheBuilder softValues() { builder.softValues(); return this; } /** * Specifies that each key (not value) stored in the cache should be wrapped * in a {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used). * *

* Warning: when this method is used, the resulting cache will use * identity ({@code ==}) comparison to determine equality of keys. * * @return This for chaining * * @throws IllegalStateException if the key strength was already set */ public SafeCacheBuilder weakKeys() { builder.weakKeys(); return this; } /** * Specifies that each value (not key) stored in the cache should be wrapped * in a {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used). * *

* Weak values will be garbage collected once they are weakly reachable. * This makes them a poor candidate for caching; consider * {@link #softValues} instead. * *

* Note: when this method is used, the resulting cache will use * identity ({@code ==}) comparison to determine equality of values. * * @return This for chaining * * @throws IllegalStateException if the value strength was already set */ public SafeCacheBuilder weakValues() { builder.weakValues(); return this; } /** * Returns the cache wrapped as a ConcurrentMap. *

* We can't return the direct Cache instance as it changed in Guava 13. * @param Key type * @param Value type * @param loader - cache loader * @return The cache as a a map. */ @SuppressWarnings("unchecked") public ConcurrentMap build(CacheLoader loader) { Object cache = null; if (BUILD_METHOD == null) { try { BUILD_METHOD = builder.getClass().getDeclaredMethod("build", CacheLoader.class); BUILD_METHOD.setAccessible(true); } catch (Exception e) { throw new FieldAccessException("Unable to find CacheBuilder.build(CacheLoader)", e); } } // Attempt to build the Cache try { cache = BUILD_METHOD.invoke(builder, loader); } catch (Exception e) { throw new FieldAccessException("Unable to invoke " + BUILD_METHOD + " on " + builder, e); } if (AS_MAP_METHOD == null) { try { AS_MAP_METHOD = cache.getClass().getMethod("asMap"); AS_MAP_METHOD.setAccessible(true); } catch (Exception e) { throw new FieldAccessException("Unable to find Cache.asMap() in " + cache, e); } } // Retrieve it as a map try { return (ConcurrentMap) AS_MAP_METHOD.invoke(cache); } catch (Exception e) { throw new FieldAccessException("Unable to invoke " + AS_MAP_METHOD + " on " + cache, e); } } }