package de.otto.edison.cache.configuration;
import com.github.benmanes.caffeine.cache.CaffeineSpec;
import org.springframework.cache.annotation.Cacheable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.github.benmanes.caffeine.cache.CaffeineSpec.parse;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
/**
* Configuration of a {@link org.springframework.cache.caffeine.CaffeineCache Caffeine Cache} with a name and a specification string.
*
* CacheConfigs should be exposed as Spring Beans. All registered instances are collected by
* {@link de.otto.edison.cache.configuration.CacheConfiguration} and used to register Caffeine caches accordingly.
*
* Spring Beans can use these caches by annotating a method as {@link org.springframework.cache.annotation.Cacheable},
* with the {@link #cacheName cache name} as value.
*
* The metrics of caches are recorded, if they are configured with "recordStats". In this case, metrics will be
* available under /internal/metrics, they will be reported according to the edison-metrics reporters, and they
* will be accessible as HTML under /internal/cacheinfos.
*
* @since 0.76.0
*/
public final class CaffeineCacheConfig {
/**
* The name of the cache.
*
* @see Cacheable#cacheNames()
*/
public final String cacheName;
/**
* The specification of the Caffeine {@link com.github.benmanes.caffeine.cache.Cache}. Used to construct
* the cache in {@link de.otto.edison.cache.configuration.CacheConfiguration} and register it
* in a {@link org.springframework.cache.CacheManager}, so it can be used using {@link Cacheable} annotations.
*/
public final CaffeineSpec spec;
/**
* Create a CaffeineCacheConfig for a named cache, using a specification string to configure the cache.
*
* @param cacheName the name of the cache. You can use the configured cache using Spring's
* {@link org.springframework.cache.annotation.Cacheable} annotation.
* @param spec A specification of a Caffeine {@link CaffeineSpec } configuration.
*
* <p>Example: "initialCapacity=1,maximumSize=5,expireAfterAccess=10s,recordStats"</p>
* <p>The string syntax is a series of comma-separated keys or key-value pairs,
* each corresponding to a {@code CacheBuilder} method.
* <ul>
* <li>{@code initialCapacity=[integer]}: sets {@link CaffeineSpec#initialCapacity}.
* <li>{@code maximumSize=[long]}: sets {@link CaffeineSpec#maximumSize}.
* <li>{@code maximumWeight=[long]}: sets {@link CaffeineSpec#maximumWeight}.
* <li>{@code expireAfterAccess=[duration]}: sets {@link CaffeineSpec#expireAfterAccess}.
* <li>{@code expireAfterWrite=[duration]}: sets {@link CaffeineSpec#expireAfterWrite}.
* <li>{@code refreshAfterWrite=[duration]}: sets {@link CaffeineSpec#refreshAfterWrite}.
* <li>{@code weakKeys}: sets {@link CaffeineSpec#weakKeys}.
* <li>{@code valueStrength}: sets {@link CaffeineSpec#valueStrength}.
* <li>{@code recordStats}: sets {@link CaffeineSpec#recordStats}.
* </ul>
*
* <p>Durations are represented by an integer, followed by one of "d", "h", "m",
* or "s", representing days, hours, minutes, or seconds respectively. (There
* is currently no syntax to request expiration in milliseconds, microseconds,
* or nanoseconds.)
*
* <p>Whitespace before and after commas and equal signs is ignored. Keys may
* not be repeated; it is also illegal to use the following pairs of keys in
* a single value:
* <ul>
* <li>{@code maximumSize} and {@code maximumWeight}
* <li>{@code softValues} and {@code weakValues}
* </ul>
*
* @since 0.76.0
*/
public CaffeineCacheConfig(final String cacheName, final String spec) {
this.cacheName = cacheName;
this.spec = parse(spec);
}
/**
*
* @return Map containing the configuration keys and values of the {@link #spec cache specification}.
*/
public Map<String,String> toMap() {
final Map<String,String> map = new LinkedHashMap<>();
for (final String keyValuePair : split(spec.toParsableString(), ",")) {
final List<String> keyAndValue = split(keyValuePair, "=");
if (keyAndValue.isEmpty()) throw new IllegalStateException("blank key-value pair");
if (keyAndValue.size() > 2) throw new IllegalStateException(format("key-value pair %s with more than one equals sign", keyValuePair));
map.put(keyAndValue.get(0), keyAndValue.size() == 1 ? "true" : keyAndValue.get(1));
}
return map;
}
private List<String> split(final String s, final String regex) {
final String[] keyValues = s.split(regex);
return keyValues != null ? asList(keyValues) : emptyList();
}
}