com.google.common.cache.CacheBuilderSpec.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.cache.CacheBuilderSpec.java

Source

/*
 * 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.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 javax.annotation.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
 */
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();

    @VisibleForTesting
    Integer initialCapacity;
    @VisibleForTesting
    Long maximumSize;
    @VisibleForTesting
    Long maximumWeight;
    @VisibleForTesting
    Integer concurrencyLevel;
    @VisibleForTesting
    Strength keyStrength;
    @VisibleForTesting
    Strength valueStrength;
    @VisibleForTesting
    Boolean recordStats;
    @VisibleForTesting
    long writeExpirationDuration;
    @VisibleForTesting
    TimeUnit writeExpirationTimeUnit;
    @VisibleForTesting
    long accessExpirationDuration;
    @VisibleForTesting
    TimeUnit accessExpirationTimeUnit;
    @VisibleForTesting
    long refreshDuration;
    @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.
     */
    @Nullable
    private static 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);
    }
}