/* * Copyright 2016 Ben Manes. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.benmanes.caffeine.cache; import static com.github.benmanes.caffeine.cache.Caffeine.UNSET_INT; import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument; import static com.github.benmanes.caffeine.cache.Caffeine.requireState; import static java.util.Objects.requireNonNull; import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import com.github.benmanes.caffeine.cache.Caffeine.Strength; /** * A specification of a {@link Caffeine} builder configuration. * <p> * {@code CaffeineSpec} supports parsing configuration off of a string, which makes it especially * useful for command-line configuration of a {@code Caffeine} builder. * <p> * The string syntax is a series of comma-separated keys or key-value pairs, each corresponding to a * {@code Caffeine} builder method. * <ul> * <li>{@code initialCapacity=[integer]}: sets {@link Caffeine#initialCapacity}. * <li>{@code maximumSize=[long]}: sets {@link Caffeine#maximumSize}. * <li>{@code maximumWeight=[long]}: sets {@link Caffeine#maximumWeight}. * <li>{@code expireAfterAccess=[duration]}: sets {@link Caffeine#expireAfterAccess}. * <li>{@code expireAfterWrite=[duration]}: sets {@link Caffeine#expireAfterWrite}. * <li>{@code refreshAfterWrite=[duration]}: sets {@link Caffeine#refreshAfterWrite}. * <li>{@code weakKeys}: sets {@link Caffeine#weakKeys}. * <li>{@code weakValues}: sets {@link Caffeine#weakValues}. * <li>{@code softValues}: sets {@link Caffeine#softValues}. * <li>{@code recordStats}: sets {@link Caffeine#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 weakValues} and {@code softValues} * </ul> * <p> * {@code CaffeineSpec} does not support configuring {@code Caffeine} methods with non-value * parameters. These must be configured in code. * <p> * A new {@code Caffeine} builder can be instantiated from a {@code CaffeineSpec} using * {@link Caffeine#from(CaffeineSpec)} or {@link Caffeine#from(String)}. * * @author ben.manes@gmail.com (Ben Manes) */ public final class CaffeineSpec { static final String SPLIT_OPTIONS = ","; static final String SPLIT_KEY_VALUE = "="; final String specification; int initialCapacity = UNSET_INT; long maximumWeight = UNSET_INT; long maximumSize = UNSET_INT; boolean recordStats; Strength keyStrength; Strength valueStrength; long expireAfterAccessDuration = UNSET_INT; TimeUnit expireAfterAccessTimeUnit; long expireAfterWriteDuration = UNSET_INT; TimeUnit expireAfterWriteTimeUnit; long refreshAfterWriteDuration = UNSET_INT; TimeUnit refreshAfterWriteTimeUnit; private CaffeineSpec(String specification) { this.specification = requireNonNull(specification); } /** * Returns a {@link Caffeine} builder configured according to this specification. * * @return a builder configured to the specification */ Caffeine<Object, Object> toBuilder() { Caffeine<Object, Object> builder = Caffeine.newBuilder(); if (initialCapacity != UNSET_INT) { builder.initialCapacity(initialCapacity); } if (maximumSize != UNSET_INT) { builder.maximumSize(maximumSize); } if (maximumWeight != UNSET_INT) { builder.maximumWeight(maximumWeight); } if (keyStrength != null) { requireState(keyStrength == Strength.WEAK); builder.weakKeys(); } if (valueStrength != null) { if (valueStrength == Strength.WEAK) { builder.weakValues(); } else if (valueStrength == Strength.SOFT) { builder.softValues(); } else { throw new IllegalStateException(); } } if (expireAfterAccessDuration != UNSET_INT) { builder.expireAfterAccess(expireAfterAccessDuration, expireAfterAccessTimeUnit); } if (expireAfterWriteDuration != UNSET_INT) { builder.expireAfterWrite(expireAfterWriteDuration, expireAfterWriteTimeUnit); } if (refreshAfterWriteDuration != UNSET_INT) { builder.refreshAfterWrite(refreshAfterWriteDuration, refreshAfterWriteTimeUnit); } if (recordStats) { builder.recordStats(); } return builder; } /** * Creates a CaffeineSpec from a string. * * @param specification the string form * @return the parsed specification */ public static CaffeineSpec parse(String specification) { CaffeineSpec spec = new CaffeineSpec(specification); for (String option : specification.split(SPLIT_OPTIONS)) { spec.parseOption(option.trim()); } return spec; } /** Parses and applies the configuration option. */ void parseOption(String option) { if (option.isEmpty()) { return; } String[] keyAndValue = option.split(SPLIT_KEY_VALUE); requireArgument(keyAndValue.length != 0, "blank key-value pair"); requireArgument(keyAndValue.length <= 2, "key-value pair %s with more than one equals sign", option); String key = keyAndValue[0].trim(); String value = (keyAndValue.length == 1) ? null : keyAndValue[1].trim(); configure(key, value); } /** Configures the setting. */ void configure(String key, @Nullable String value) { switch (key) { case "initialCapacity": initialCapacity(key, value); return; case "maximumSize": maximumSize(key, value); return; case "maximumWeight": maximumWeight(key, value); return; case "weakKeys": weakKeys(value); return; case "weakValues": valueStrength(key, value, Strength.WEAK); return; case "softValues": valueStrength(key, value, Strength.SOFT); return; case "expireAfterAccess": expireAfterAccess(key, value); return; case "expireAfterWrite": expireAfterWrite(key, value); return; case "refreshAfterWrite": refreshAfterWrite(key, value); return; case "recordStats": recordStats(value); return; default: throw new IllegalArgumentException("Unknown key " + key); } } /** Configures the initial capacity. */ void initialCapacity(String key, String value) { requireArgument(initialCapacity == UNSET_INT, "initial capacity was already set to %,d", initialCapacity); initialCapacity = parseInt(key, value); } /** Configures the maximum size. */ void maximumSize(String key, String value) { requireArgument(maximumSize == UNSET_INT, "maximum size was already set to %,d", maximumSize); requireArgument(maximumWeight == UNSET_INT, "maximum weight was already set to %,d", maximumWeight); maximumSize = parseLong(key, value); } /** Configures the maximum size. */ void maximumWeight(String key, String value) { requireArgument(maximumWeight == UNSET_INT, "maximum weight was already set to %,d", maximumWeight); requireArgument(maximumSize == UNSET_INT, "maximum size was already set to %,d", maximumSize); maximumWeight = parseLong(key, value); } /** Configures the keys as weak references. */ void weakKeys(@Nullable String value) { requireArgument(value == null, "weak keys does not take a value"); requireArgument(keyStrength == null, "weak keys was already set"); keyStrength = Strength.WEAK; } /** Configures the value as weak or soft references. */ void valueStrength(String key, @Nullable String value, Strength strength) { requireArgument(value == null, "%s does not take a value", key); requireArgument(valueStrength == null, "%s was already set to %s", key, valueStrength); valueStrength = strength; } /** Configures expire after access. */ void expireAfterAccess(String key, String value) { requireArgument(expireAfterAccessDuration == UNSET_INT, "expireAfterAccess was already set"); expireAfterAccessTimeUnit = parseTimeUnit(key, value); expireAfterAccessDuration = parseDuration(key, value); } /** Configures expire after write. */ void expireAfterWrite(String key, String value) { requireArgument(expireAfterWriteDuration == UNSET_INT, "expireAfterWrite was already set"); expireAfterWriteTimeUnit = parseTimeUnit(key, value); expireAfterWriteDuration = parseDuration(key, value); } /** Configures refresh after write. */ void refreshAfterWrite(String key, String value) { requireArgument(refreshAfterWriteDuration == UNSET_INT, "refreshAfterWrite was already set"); refreshAfterWriteTimeUnit = parseTimeUnit(key, value); refreshAfterWriteDuration = parseDuration(key, value); } /** Configures the value as weak or soft references. */ void recordStats(@Nullable String value) { requireArgument(value == null, "record stats does not take a value"); requireArgument(!recordStats, "record stats was already set"); recordStats = true; } /** Returns a parsed int value. */ static int parseInt(String key, String value) { requireArgument(value != null && !value.isEmpty(), "value of key %s was omitted", key); try { return Integer.parseInt(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(String.format( "key %s value was set to %s, must be an integer", key, value), e); } } /** Returns a parsed long value. */ static long parseLong(String key, String value) { requireArgument(value != null && !value.isEmpty(), "value of key %s was omitted", key); try { return Long.parseLong(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(String.format( "key %s value was set to %s, must be a long", key, value), e); } } /** Returns a parsed duration value. */ static long parseDuration(String key, String value) { requireArgument(value != null && !value.isEmpty(), "value of key %s omitted", key); return parseLong(key, value.substring(0, value.length() - 1)); } /** Returns a parsed {@link TimeUnit} value. */ static TimeUnit parseTimeUnit(String key, String value) { requireArgument(value != null && !value.isEmpty(), "value of key %s omitted", key); char lastChar = Character.toLowerCase(value.charAt(value.length() - 1)); switch (lastChar) { case 'd': return TimeUnit.DAYS; case 'h': return TimeUnit.HOURS; case 'm': return TimeUnit.MINUTES; case 's': return TimeUnit.SECONDS; default: throw new IllegalArgumentException(String.format( "key %s invalid format; was %s, must end with one of [dDhHmMsS]", key, value)); } } @Override public boolean equals(Object o) { if (this == o) { return true; } else if (!(o instanceof CaffeineSpec)) { return false; } CaffeineSpec spec = (CaffeineSpec) o; return Objects.equals(initialCapacity, spec.initialCapacity) && Objects.equals(maximumSize, spec.maximumSize) && Objects.equals(maximumWeight, spec.maximumWeight) && Objects.equals(keyStrength, spec.keyStrength) && Objects.equals(valueStrength, spec.valueStrength) && Objects.equals(recordStats, spec.recordStats) && (durationInNanos(expireAfterAccessDuration, expireAfterAccessTimeUnit) == durationInNanos(spec.expireAfterAccessDuration, spec.expireAfterAccessTimeUnit)) && (durationInNanos(expireAfterWriteDuration, expireAfterWriteTimeUnit) == durationInNanos(spec.expireAfterWriteDuration, spec.expireAfterWriteTimeUnit)) && (durationInNanos(refreshAfterWriteDuration, refreshAfterWriteTimeUnit) == durationInNanos(spec.refreshAfterWriteDuration, spec.refreshAfterWriteTimeUnit)); } @Override public int hashCode() { return Objects.hash( initialCapacity, maximumSize, maximumWeight, keyStrength, valueStrength, recordStats, durationInNanos(expireAfterAccessDuration, expireAfterAccessTimeUnit), durationInNanos(expireAfterWriteDuration, expireAfterWriteTimeUnit), durationInNanos(refreshAfterWriteDuration, refreshAfterWriteTimeUnit)); } /** Converts an expiration duration/unit pair into a single long for hashing and equality. */ static long durationInNanos(long duration, @Nullable TimeUnit unit) { return (unit == null) ? UNSET_INT : unit.toNanos(duration); } /** * Returns a string that can be used to parse an equivalent {@code CaffeineSpec}. The order and * form of this representation is not guaranteed, except that parsing its output will produce a * {@code CaffeineSpec} equal to this instance. * * @return a string representation of this specification */ public String toParsableString() { return specification; } /** * Returns a string representation for this {@code CaffeineSpec} instance. The form of this * representation is not guaranteed. */ @Override public String toString() { return getClass().getSimpleName() + '{' + toParsableString() + '}'; } }