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