| /* |
| * Copyright (C) 2011 The Guava Authors |
| * |
| * 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.google.common.cache; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import com.google.common.annotations.GwtIncompatible; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Splitter; |
| import com.google.common.cache.LocalCache.Strength; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.concurrent.TimeUnit; |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| |
| /** |
| * A specification of a {@link CacheBuilder} configuration. |
| * |
| * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which makes it |
| * especially useful for command-line configuration of a {@code CacheBuilder}. |
| * |
| * <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 concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}. |
| * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}. |
| * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}. |
| * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}. |
| * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}. |
| * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}. |
| * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}. |
| * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}. |
| * <li>{@code softValues}: sets {@link CacheBuilder#softValues}. |
| * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}. |
| * <li>{@code recordStats}: sets {@link CacheBuilder#recordStats}. |
| * </ul> |
| * |
| * <p>The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys will |
| * never be removed. |
| * |
| * <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> |
| * |
| * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods with |
| * non-value parameters. These must be configured in code. |
| * |
| * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using {@link |
| * CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}. |
| * |
| * @author Adam Winer |
| * @since 12.0 |
| */ |
| @SuppressWarnings("GoodTime") // lots of violations (nanosecond math) |
| @GwtIncompatible |
| public final class CacheBuilderSpec { |
| /** Parses a single value. */ |
| private interface ValueParser { |
| void parse(CacheBuilderSpec spec, String key, @Nullable String value); |
| } |
| |
| /** Splits each key-value pair. */ |
| private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults(); |
| |
| /** Splits the key from the value. */ |
| private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults(); |
| |
| /** Map of names to ValueParser. */ |
| private static final ImmutableMap<String, ValueParser> VALUE_PARSERS = |
| ImmutableMap.<String, ValueParser>builder() |
| .put("initialCapacity", new InitialCapacityParser()) |
| .put("maximumSize", new MaximumSizeParser()) |
| .put("maximumWeight", new MaximumWeightParser()) |
| .put("concurrencyLevel", new ConcurrencyLevelParser()) |
| .put("weakKeys", new KeyStrengthParser(Strength.WEAK)) |
| .put("softValues", new ValueStrengthParser(Strength.SOFT)) |
| .put("weakValues", new ValueStrengthParser(Strength.WEAK)) |
| .put("recordStats", new RecordStatsParser()) |
| .put("expireAfterAccess", new AccessDurationParser()) |
| .put("expireAfterWrite", new WriteDurationParser()) |
| .put("refreshAfterWrite", new RefreshDurationParser()) |
| .put("refreshInterval", new RefreshDurationParser()) |
| .build(); |
| |
| @MonotonicNonNull @VisibleForTesting Integer initialCapacity; |
| @MonotonicNonNull @VisibleForTesting Long maximumSize; |
| @MonotonicNonNull @VisibleForTesting Long maximumWeight; |
| @MonotonicNonNull @VisibleForTesting Integer concurrencyLevel; |
| @MonotonicNonNull @VisibleForTesting Strength keyStrength; |
| @MonotonicNonNull @VisibleForTesting Strength valueStrength; |
| @MonotonicNonNull @VisibleForTesting Boolean recordStats; |
| @VisibleForTesting long writeExpirationDuration; |
| @MonotonicNonNull @VisibleForTesting TimeUnit writeExpirationTimeUnit; |
| @VisibleForTesting long accessExpirationDuration; |
| @MonotonicNonNull @VisibleForTesting TimeUnit accessExpirationTimeUnit; |
| @VisibleForTesting long refreshDuration; |
| @MonotonicNonNull @VisibleForTesting TimeUnit refreshTimeUnit; |
| /** Specification; used for toParseableString(). */ |
| private final String specification; |
| |
| private CacheBuilderSpec(String specification) { |
| this.specification = specification; |
| } |
| |
| /** |
| * Creates a CacheBuilderSpec from a string. |
| * |
| * @param cacheBuilderSpecification the string form |
| */ |
| public static CacheBuilderSpec parse(String cacheBuilderSpecification) { |
| CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification); |
| if (!cacheBuilderSpecification.isEmpty()) { |
| for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) { |
| List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair)); |
| checkArgument(!keyAndValue.isEmpty(), "blank key-value pair"); |
| checkArgument( |
| keyAndValue.size() <= 2, |
| "key-value pair %s with more than one equals sign", |
| keyValuePair); |
| |
| // Find the ValueParser for the current key. |
| String key = keyAndValue.get(0); |
| ValueParser valueParser = VALUE_PARSERS.get(key); |
| checkArgument(valueParser != null, "unknown key %s", key); |
| |
| String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1); |
| valueParser.parse(spec, key, value); |
| } |
| } |
| |
| return spec; |
| } |
| |
| /** Returns a CacheBuilderSpec that will prevent caching. */ |
| public static CacheBuilderSpec disableCaching() { |
| // Maximum size of zero is one way to block caching |
| return CacheBuilderSpec.parse("maximumSize=0"); |
| } |
| |
| /** Returns a CacheBuilder configured according to this instance's specification. */ |
| CacheBuilder<Object, Object> toCacheBuilder() { |
| CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder(); |
| if (initialCapacity != null) { |
| builder.initialCapacity(initialCapacity); |
| } |
| if (maximumSize != null) { |
| builder.maximumSize(maximumSize); |
| } |
| if (maximumWeight != null) { |
| builder.maximumWeight(maximumWeight); |
| } |
| if (concurrencyLevel != null) { |
| builder.concurrencyLevel(concurrencyLevel); |
| } |
| if (keyStrength != null) { |
| switch (keyStrength) { |
| case WEAK: |
| builder.weakKeys(); |
| break; |
| default: |
| throw new AssertionError(); |
| } |
| } |
| if (valueStrength != null) { |
| switch (valueStrength) { |
| case SOFT: |
| builder.softValues(); |
| break; |
| case WEAK: |
| builder.weakValues(); |
| break; |
| default: |
| throw new AssertionError(); |
| } |
| } |
| if (recordStats != null && recordStats) { |
| builder.recordStats(); |
| } |
| if (writeExpirationTimeUnit != null) { |
| builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit); |
| } |
| if (accessExpirationTimeUnit != null) { |
| builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit); |
| } |
| if (refreshTimeUnit != null) { |
| builder.refreshAfterWrite(refreshDuration, refreshTimeUnit); |
| } |
| |
| return builder; |
| } |
| |
| /** |
| * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order |
| * and form of this representation is not guaranteed, except that reparsing its output will |
| * produce a {@code CacheBuilderSpec} equal to this instance. |
| */ |
| public String toParsableString() { |
| return specification; |
| } |
| |
| /** |
| * Returns a string representation for this CacheBuilderSpec instance. The form of this |
| * representation is not guaranteed. |
| */ |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode( |
| initialCapacity, |
| maximumSize, |
| maximumWeight, |
| concurrencyLevel, |
| keyStrength, |
| valueStrength, |
| recordStats, |
| durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), |
| durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), |
| durationInNanos(refreshDuration, refreshTimeUnit)); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof CacheBuilderSpec)) { |
| return false; |
| } |
| CacheBuilderSpec that = (CacheBuilderSpec) obj; |
| return Objects.equal(initialCapacity, that.initialCapacity) |
| && Objects.equal(maximumSize, that.maximumSize) |
| && Objects.equal(maximumWeight, that.maximumWeight) |
| && Objects.equal(concurrencyLevel, that.concurrencyLevel) |
| && Objects.equal(keyStrength, that.keyStrength) |
| && Objects.equal(valueStrength, that.valueStrength) |
| && Objects.equal(recordStats, that.recordStats) |
| && Objects.equal( |
| durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), |
| durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit)) |
| && Objects.equal( |
| durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), |
| durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit)) |
| && Objects.equal( |
| durationInNanos(refreshDuration, refreshTimeUnit), |
| durationInNanos(that.refreshDuration, that.refreshTimeUnit)); |
| } |
| |
| /** |
| * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses |
| * nanos to match CacheBuilder implementation. |
| */ |
| private static @Nullable Long durationInNanos(long duration, @Nullable TimeUnit unit) { |
| return (unit == null) ? null : unit.toNanos(duration); |
| } |
| |
| /** Base class for parsing integers. */ |
| abstract static class IntegerParser implements ValueParser { |
| protected abstract void parseInteger(CacheBuilderSpec spec, int value); |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, String value) { |
| checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key); |
| try { |
| parseInteger(spec, Integer.parseInt(value)); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException( |
| format("key %s value set to %s, must be integer", key, value), e); |
| } |
| } |
| } |
| |
| /** Base class for parsing integers. */ |
| abstract static class LongParser implements ValueParser { |
| protected abstract void parseLong(CacheBuilderSpec spec, long value); |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, String value) { |
| checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key); |
| try { |
| parseLong(spec, Long.parseLong(value)); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException( |
| format("key %s value set to %s, must be integer", key, value), e); |
| } |
| } |
| } |
| |
| /** Parse initialCapacity */ |
| static class InitialCapacityParser extends IntegerParser { |
| @Override |
| protected void parseInteger(CacheBuilderSpec spec, int value) { |
| checkArgument( |
| spec.initialCapacity == null, |
| "initial capacity was already set to ", |
| spec.initialCapacity); |
| spec.initialCapacity = value; |
| } |
| } |
| |
| /** Parse maximumSize */ |
| static class MaximumSizeParser extends LongParser { |
| @Override |
| protected void parseLong(CacheBuilderSpec spec, long value) { |
| checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize); |
| checkArgument( |
| spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight); |
| spec.maximumSize = value; |
| } |
| } |
| |
| /** Parse maximumWeight */ |
| static class MaximumWeightParser extends LongParser { |
| @Override |
| protected void parseLong(CacheBuilderSpec spec, long value) { |
| checkArgument( |
| spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight); |
| checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize); |
| spec.maximumWeight = value; |
| } |
| } |
| |
| /** Parse concurrencyLevel */ |
| static class ConcurrencyLevelParser extends IntegerParser { |
| @Override |
| protected void parseInteger(CacheBuilderSpec spec, int value) { |
| checkArgument( |
| spec.concurrencyLevel == null, |
| "concurrency level was already set to ", |
| spec.concurrencyLevel); |
| spec.concurrencyLevel = value; |
| } |
| } |
| |
| /** Parse weakKeys */ |
| static class KeyStrengthParser implements ValueParser { |
| private final Strength strength; |
| |
| public KeyStrengthParser(Strength strength) { |
| this.strength = strength; |
| } |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { |
| checkArgument(value == null, "key %s does not take values", key); |
| checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength); |
| spec.keyStrength = strength; |
| } |
| } |
| |
| /** Parse weakValues and softValues */ |
| static class ValueStrengthParser implements ValueParser { |
| private final Strength strength; |
| |
| public ValueStrengthParser(Strength strength) { |
| this.strength = strength; |
| } |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { |
| checkArgument(value == null, "key %s does not take values", key); |
| checkArgument( |
| spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength); |
| |
| spec.valueStrength = strength; |
| } |
| } |
| |
| /** Parse recordStats */ |
| static class RecordStatsParser implements ValueParser { |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { |
| checkArgument(value == null, "recordStats does not take values"); |
| checkArgument(spec.recordStats == null, "recordStats already set"); |
| spec.recordStats = true; |
| } |
| } |
| |
| /** Base class for parsing times with durations */ |
| abstract static class DurationParser implements ValueParser { |
| protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit); |
| |
| @Override |
| public void parse(CacheBuilderSpec spec, String key, String value) { |
| checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key); |
| try { |
| char lastChar = value.charAt(value.length() - 1); |
| TimeUnit timeUnit; |
| switch (lastChar) { |
| case 'd': |
| timeUnit = TimeUnit.DAYS; |
| break; |
| case 'h': |
| timeUnit = TimeUnit.HOURS; |
| break; |
| case 'm': |
| timeUnit = TimeUnit.MINUTES; |
| break; |
| case 's': |
| timeUnit = TimeUnit.SECONDS; |
| break; |
| default: |
| throw new IllegalArgumentException( |
| format( |
| "key %s invalid format. was %s, must end with one of [dDhHmMsS]", key, value)); |
| } |
| |
| long duration = Long.parseLong(value.substring(0, value.length() - 1)); |
| parseDuration(spec, duration, timeUnit); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException( |
| format("key %s value set to %s, must be integer", key, value)); |
| } |
| } |
| } |
| |
| /** Parse expireAfterAccess */ |
| static class AccessDurationParser extends DurationParser { |
| @Override |
| protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { |
| checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set"); |
| spec.accessExpirationDuration = duration; |
| spec.accessExpirationTimeUnit = unit; |
| } |
| } |
| |
| /** Parse expireAfterWrite */ |
| static class WriteDurationParser extends DurationParser { |
| @Override |
| protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { |
| checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set"); |
| spec.writeExpirationDuration = duration; |
| spec.writeExpirationTimeUnit = unit; |
| } |
| } |
| |
| /** Parse refreshAfterWrite */ |
| static class RefreshDurationParser extends DurationParser { |
| @Override |
| protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { |
| checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set"); |
| spec.refreshDuration = duration; |
| spec.refreshTimeUnit = unit; |
| } |
| } |
| |
| private static String format(String format, Object... args) { |
| return String.format(Locale.ROOT, format, args); |
| } |
| } |