    001    /*
    002     * Copyright (C) 2011 The Guava Authors
    003     *
    004     * Licensed under the Apache License, Version 2.0 (the "License");
    005     * you may not use this file except in compliance with the License.
    006     * You may obtain a copy of the License at
    007     *
    008     * http://www.apache.org/licenses/LICENSE-2.0
    009     *
    010     * Unless required by applicable law or agreed to in writing, software
    011     * distributed under the License is distributed on an "AS IS" BASIS,
    012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013     * See the License for the specific language governing permissions and
    014     * limitations under the License.
    015     */
    016
    017    package com.google.common.cache;
    018
    019    import static com.google.common.base.Preconditions.checkArgument;
    020
    021    import com.google.common.annotations.Beta;
    022    import com.google.common.annotations.VisibleForTesting;
    023    import com.google.common.base.Objects;
    024    import com.google.common.base.Splitter;
    025    import com.google.common.cache.LocalCache.Strength;
    026    import com.google.common.collect.ImmutableList;
    027    import com.google.common.collect.ImmutableMap;
    028
    029    import java.util.List;
    030    import java.util.concurrent.TimeUnit;
    031
    032    import javax.annotation.Nullable;
    033
    034    /**
    035     * A specification of a {@link CacheBuilder} configuration.
    036     *
    037     * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
    038     * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
    039     *
    040     * <p>The string syntax is a series of comma-separated keys or key-value pairs,
    041     * each corresponding to a {@code CacheBuilder} method.
    042     * <ul>
    043     * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
    044     * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
    045     * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
    046     * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
    047     * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
    048     * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
    049     * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
    050     * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
    051     * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
    052     * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
    053     * </ul>
    054     *
    055     * The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys
    056     * will never be removed.
    057     *
    058     * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
    059     * or "s", representing days, hours, minutes, or seconds respectively.  (There
    060     * is currently no syntax to request expiration in milliseconds, microseconds,
    061     * or nanoseconds.)
    062     *
    063     * <p>Whitespace before and after commas and equal signs is ignored.  Keys may
    064     * not be repeated;  it is also illegal to use the following pairs of keys in
    065     * a single value:
    066     * <ul>
    067     * <li>{@code maximumSize} and {@code maximumWeight}
    068     * <li>{@code softValues} and {@code weakValues}
    069     * </ul>
    070     *
    071     * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
    072     * with non-value parameters.  These must be configured in code.
    073     *
    074     * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
    075     * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
    076     *
    077     * @author Adam Winer
    078     * @since 12.0
    079     */
    080    @Beta
    081    public final class CacheBuilderSpec {
    082      /** Parses a single value. */
    083      private interface ValueParser {
    084        void parse(CacheBuilderSpec spec, String key, @Nullable String value);
    085      }
    086
    087      /** Splits each key-value pair. */
    088      private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
    089
    090      /** Splits the key from the value. */
    091      private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
    092
    093      /** Map of names to ValueParser. */
    094      private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
    095          ImmutableMap.<String, ValueParser>builder()
    096              .put("initialCapacity", new InitialCapacityParser())
    097              .put("maximumSize", new MaximumSizeParser())
    098              .put("maximumWeight", new MaximumWeightParser())
    099              .put("concurrencyLevel", new ConcurrencyLevelParser())
    100              .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
    101              .put("softValues", new ValueStrengthParser(Strength.SOFT))
    102              .put("weakValues", new ValueStrengthParser(Strength.WEAK))
    103              .put("expireAfterAccess", new AccessDurationParser())
    104              .put("expireAfterWrite", new WriteDurationParser())
    105              .put("refreshAfterWrite", new RefreshDurationParser())
    106              .put("refreshInterval", new RefreshDurationParser())
    107              .build();
    108
    109      @VisibleForTesting Integer initialCapacity;
    110      @VisibleForTesting Long maximumSize;
    111      @VisibleForTesting Long maximumWeight;
    112      @VisibleForTesting Integer concurrencyLevel;
    113      @VisibleForTesting Strength keyStrength;
    114      @VisibleForTesting Strength valueStrength;
    115      @VisibleForTesting long writeExpirationDuration;
    116      @VisibleForTesting TimeUnit writeExpirationTimeUnit;
    117      @VisibleForTesting long accessExpirationDuration;
    118      @VisibleForTesting TimeUnit accessExpirationTimeUnit;
    119      @VisibleForTesting long refreshDuration;
    120      @VisibleForTesting TimeUnit refreshTimeUnit;
    121      /** Specification;  used for toParseableString(). */
    122      private final String specification;
    123
    124      private CacheBuilderSpec(String specification) {
    125        this.specification = specification;
    126      }
    127
    128      /**
    129       * Creates a CacheBuilderSpec from a string.
    130       *
    131       * @param cacheBuilderSpecification the string form
    132       */
    133      public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
    134        CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
    135        if (!cacheBuilderSpecification.isEmpty()) {
    136          for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
    137            List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
    138            checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
    139            checkArgument(keyAndValue.size() <= 2,
    140                "key-value pair %s with more than one equals sign", keyValuePair);
    141
    142            // Find the ValueParser for the current key.
    143            String key = keyAndValue.get(0);
    144            ValueParser valueParser = VALUE_PARSERS.get(key);
    145            checkArgument(valueParser != null, "unknown key %s", key);
    146
    147            String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
    148            valueParser.parse(spec, key, value);
    149          }
    150        }
    151
    152        return spec;
    153      }
    154
    155      /**
    156       * Returns a CacheBuilderSpec that will prevent caching.
    157       */
    158      public static CacheBuilderSpec disableCaching() {
    159        // Maximum size of zero is one way to block caching
    160        return CacheBuilderSpec.parse("maximumSize=0");
    161      }
    162
    163      /**
    164       * Returns a CacheBuilder configured according to this instance's specification.
    165       */
    166      CacheBuilder<Object, Object> toCacheBuilder() {
    167        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
    168        if (initialCapacity != null) {
    169          builder.initialCapacity(initialCapacity);
    170        }
    171        if (maximumSize != null) {
    172          builder.maximumSize(maximumSize);
    173        }
    174        if (maximumWeight != null) {
    175          builder.maximumWeight(maximumWeight);
    176        }
    177        if (concurrencyLevel != null) {
    178          builder.concurrencyLevel(concurrencyLevel);
    179        }
    180        if (keyStrength != null) {
    181          switch (keyStrength) {
    182            case WEAK:
    183              builder.weakKeys();
    184              break;
    185            default:
    186              throw new AssertionError();
    187          }
    188        }
    189        if (valueStrength != null) {
    190          switch (valueStrength) {
    191            case SOFT:
    192              builder.softValues();
    193              break;
    194            case WEAK:
    195              builder.weakValues();
    196              break;
    197            default:
    198              throw new AssertionError();
    199          }
    200        }
    201        if (writeExpirationTimeUnit != null) {
    202          builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
    203        }
    204        if (accessExpirationTimeUnit != null) {
    205          builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
    206        }
    207        if (refreshTimeUnit != null) {
    208          builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
    209        }
    210
    211        return builder;
    212      }
    213
    214      /**
    215       * Returns a string that can be used to parse an equivalent
    216       * {@code CacheBuilderSpec}.  The order and form of this representation is
    217       * not guaranteed, except that reparsing its output will produce
    218       * a {@code CacheBuilderSpec} equal to this instance.
    219       */
    220      public String toParsableString() {
    221        return specification;
    222      }
    223
    224      /**
    225       * Returns a string representation for this CacheBuilderSpec instance.
    226       * The form of this representation is not guaranteed.
    227       */
    228      @Override
    229      public String toString() {
    230        return Objects.toStringHelper(this).addValue(toParsableString()).toString();
    231      }
    232
    233      @Override
    234      public int hashCode() {
    235        return Objects.hashCode(
    236            initialCapacity,
    237            maximumSize,
    238            maximumWeight,
    239            concurrencyLevel,
    240            keyStrength,
    241            valueStrength,
    242            durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
    243            durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
    244            durationInNanos(refreshDuration, refreshTimeUnit));
    245      }
    246
    247      @Override
    248      public boolean equals(Object obj) {
    249        if (this == obj) {
    250          return true;
    251        }
    252        if (!(obj instanceof CacheBuilderSpec)) {
    253          return false;
    254        }
    255        CacheBuilderSpec that = (CacheBuilderSpec) obj;
    256        return Objects.equal(initialCapacity, that.initialCapacity)
    257            && Objects.equal(maximumSize, that.maximumSize)
    258            && Objects.equal(maximumWeight, that.maximumWeight)
    259            && Objects.equal(concurrencyLevel, that.concurrencyLevel)
    260            && Objects.equal(keyStrength, that.keyStrength)
    261            && Objects.equal(valueStrength, that.valueStrength)
    262            && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
    263                durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
    264            && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
    265                durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
    266            && Objects.equal(durationInNanos(refreshDuration, refreshTimeUnit),
    267                durationInNanos(that.refreshDuration, that.refreshTimeUnit));
    268      }
    269
    270      /**
    271       * Converts an expiration duration/unit pair into a single Long for hashing and equality.
    272       * Uses nanos to match CacheBuilder implementation.
    273       */
    274      @Nullable private static Long durationInNanos(long duration, @Nullable TimeUnit unit) {
    275        return (unit == null) ? null : unit.toNanos(duration);
    276      }
    277
    278      /** Base class for parsing integers. */
    279      abstract static class IntegerParser implements ValueParser {
    280        protected abstract void parseInteger(CacheBuilderSpec spec, int value);
    281
    282        @Override
    283        public void parse(CacheBuilderSpec spec, String key, String value) {
    284          checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
    285          try {
    286            parseInteger(spec, Integer.parseInt(value));
    287          } catch (NumberFormatException e) {
    288            throw new IllegalArgumentException(
    289                String.format("key %s value set to %s, must be integer", key, value), e);
    290          }
    291        }
    292      }
    293
    294      /** Base class for parsing integers. */
    295      abstract static class LongParser implements ValueParser {
    296        protected abstract void parseLong(CacheBuilderSpec spec, long value);
    297
    298        @Override
    299        public void parse(CacheBuilderSpec spec, String key, String value) {
    300          checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
    301          try {
    302            parseLong(spec, Long.parseLong(value));
    303          } catch (NumberFormatException e) {
    304            throw new IllegalArgumentException(
    305                String.format("key %s value set to %s, must be integer", key, value), e);
    306          }
    307        }
    308      }
    309
    310      /** Parse initialCapacity */
    311      static class InitialCapacityParser extends IntegerParser {
    312        @Override
    313        protected void parseInteger(CacheBuilderSpec spec, int value) {
    314          checkArgument(spec.initialCapacity == null,
    315              "initial capacity was already set to ", spec.initialCapacity);
    316          spec.initialCapacity = value;
    317        }
    318      }
    319
    320      /** Parse maximumSize */
    321      static class MaximumSizeParser extends LongParser {
    322        @Override
    323        protected void parseLong(CacheBuilderSpec spec, long value) {
    324          checkArgument(spec.maximumSize == null,
    325              "maximum size was already set to ", spec.maximumSize);
    326          checkArgument(spec.maximumWeight == null,
    327              "maximum weight was already set to ", spec.maximumWeight);
    328          spec.maximumSize = value;
    329        }
    330      }
    331
    332      /** Parse maximumWeight */
    333      static class MaximumWeightParser extends LongParser {
    334        @Override
    335        protected void parseLong(CacheBuilderSpec spec, long value) {
    336          checkArgument(spec.maximumWeight == null,
    337              "maximum weight was already set to ", spec.maximumWeight);
    338          checkArgument(spec.maximumSize == null,
    339              "maximum size was already set to ", spec.maximumSize);
    340          spec.maximumWeight = value;
    341        }
    342      }
    343
    344      /** Parse concurrencyLevel */
    345      static class ConcurrencyLevelParser extends IntegerParser {
    346        @Override
    347        protected void parseInteger(CacheBuilderSpec spec, int value) {
    348          checkArgument(spec.concurrencyLevel == null,
    349              "concurrency level was already set to ", spec.concurrencyLevel);
    350          spec.concurrencyLevel = value;
    351        }
    352      }
    353
    354      /** Parse weakKeys */
    355      static class KeyStrengthParser implements ValueParser {
    356        private final Strength strength;
    357
    358        public KeyStrengthParser(Strength strength) {
    359          this.strength = strength;
    360        }
    361
    362        @Override
    363        public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
    364          checkArgument(value == null, "key %s does not take values", key);
    365          checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
    366          spec.keyStrength = strength;
    367        }
    368      }
    369
    370      /** Parse weakValues and softValues */
    371      static class ValueStrengthParser implements ValueParser {
    372        private final Strength strength;
    373
    374        public ValueStrengthParser(Strength strength) {
    375          this.strength = strength;
    376        }
    377
    378        @Override
    379        public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
    380          checkArgument(value == null, "key %s does not take values", key);
    381          checkArgument(spec.valueStrength == null,
    382            "%s was already set to %s", key, spec.valueStrength);
    383
    384          spec.valueStrength = strength;
    385        }
    386      }
    387
    388      /** Base class for parsing times with durations */
    389      abstract static class DurationParser implements ValueParser {
    390        protected abstract void parseDuration(
    391            CacheBuilderSpec spec,
    392            long duration,
    393            TimeUnit unit);
    394
    395        @Override
    396        public void parse(CacheBuilderSpec spec, String key, String value) {
    397          checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
    398          try {
    399            char lastChar = value.charAt(value.length() - 1);
    400            TimeUnit timeUnit;
    401            switch (lastChar) {
    402              case 'd':
    403                timeUnit = TimeUnit.DAYS;
    404                break;
    405              case 'h':
    406                timeUnit = TimeUnit.HOURS;
    407                break;
    408              case 'm':
    409                timeUnit = TimeUnit.MINUTES;
    410                break;
    411              case 's':
    412                timeUnit = TimeUnit.SECONDS;
    413                break;
    414              default:
    415                throw new IllegalArgumentException(
    416                    String.format("key %s invalid format.  was %s, must end with one of [dDhHmMsS]",
    417                        key, value));
    418            }
    419
    420            long duration = Long.parseLong(value.substring(0, value.length() - 1));
    421            parseDuration(spec, duration, timeUnit);
    422          } catch (NumberFormatException e) {
    423            throw new IllegalArgumentException(
    424                String.format("key %s value set to %s, must be integer", key, value));
    425          }
    426        }
    427      }
    428
    429      /** Parse expireAfterAccess */
    430      static class AccessDurationParser extends DurationParser {
    431        @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
    432          checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
    433          spec.accessExpirationDuration = duration;
    434          spec.accessExpirationTimeUnit = unit;
    435        }
    436      }
    437
    438      /** Parse expireAfterWrite */
    439      static class WriteDurationParser extends DurationParser {
    440        @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
    441          checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
    442          spec.writeExpirationDuration = duration;
    443          spec.writeExpirationTimeUnit = unit;
    444        }
    445      }
    446
    447      /** Parse refreshAfterWrite */
    448      static class RefreshDurationParser extends DurationParser {
    449        @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
    450          checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
    451          spec.refreshDuration = duration;
    452          spec.refreshTimeUnit = unit;
    453        }
    454      }
    455    }