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.qual.MonotonicNonNull;
031import org.checkerframework.checker.nullness.qual.Nullable;
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@SuppressWarnings("GoodTime") // lots of violations (nanosecond math)
081@GwtIncompatible
082public final class CacheBuilderSpec {
083  /** Parses a single value. */
084  private interface ValueParser {
085    void parse(CacheBuilderSpec spec, String key, @Nullable String value);
086  }
087
088  /** Splits each key-value pair. */
089  private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
090
091  /** Splits the key from the value. */
092  private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
093
094  /** Map of names to ValueParser. */
095  private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
096      ImmutableMap.<String, ValueParser>builder()
097          .put("initialCapacity", new InitialCapacityParser())
098          .put("maximumSize", new MaximumSizeParser())
099          .put("maximumWeight", new MaximumWeightParser())
100          .put("concurrencyLevel", new ConcurrencyLevelParser())
101          .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
102          .put("softValues", new ValueStrengthParser(Strength.SOFT))
103          .put("weakValues", new ValueStrengthParser(Strength.WEAK))
104          .put("recordStats", new RecordStatsParser())
105          .put("expireAfterAccess", new AccessDurationParser())
106          .put("expireAfterWrite", new WriteDurationParser())
107          .put("refreshAfterWrite", new RefreshDurationParser())
108          .put("refreshInterval", new RefreshDurationParser())
109          .build();
110
111  @MonotonicNonNull @VisibleForTesting Integer initialCapacity;
112  @MonotonicNonNull @VisibleForTesting Long maximumSize;
113  @MonotonicNonNull @VisibleForTesting Long maximumWeight;
114  @MonotonicNonNull @VisibleForTesting Integer concurrencyLevel;
115  @MonotonicNonNull @VisibleForTesting Strength keyStrength;
116  @MonotonicNonNull @VisibleForTesting Strength valueStrength;
117  @MonotonicNonNull @VisibleForTesting Boolean recordStats;
118  @VisibleForTesting long writeExpirationDuration;
119  @MonotonicNonNull @VisibleForTesting TimeUnit writeExpirationTimeUnit;
120  @VisibleForTesting long accessExpirationDuration;
121  @MonotonicNonNull @VisibleForTesting TimeUnit accessExpirationTimeUnit;
122  @VisibleForTesting long refreshDuration;
123  @MonotonicNonNull @VisibleForTesting TimeUnit refreshTimeUnit;
124  /** Specification; used for toParseableString(). */
125  private final String specification;
126
127  private CacheBuilderSpec(String specification) {
128    this.specification = specification;
129  }
130
131  /**
132   * Creates a CacheBuilderSpec from a string.
133   *
134   * @param cacheBuilderSpecification the string form
135   */
136  public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
137    CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
138    if (!cacheBuilderSpecification.isEmpty()) {
139      for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
140        List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
141        checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
142        checkArgument(
143            keyAndValue.size() <= 2,
144            "key-value pair %s with more than one equals sign",
145            keyValuePair);
146
147        // Find the ValueParser for the current key.
148        String key = keyAndValue.get(0);
149        ValueParser valueParser = VALUE_PARSERS.get(key);
150        checkArgument(valueParser != null, "unknown key %s", key);
151
152        String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
153        valueParser.parse(spec, key, value);
154      }
155    }
156
157    return spec;
158  }
159
160  /** Returns a CacheBuilderSpec that will prevent caching. */
161  public static CacheBuilderSpec disableCaching() {
162    // Maximum size of zero is one way to block caching
163    return CacheBuilderSpec.parse("maximumSize=0");
164  }
165
166  /** Returns a CacheBuilder configured according to this instance's specification. */
167  CacheBuilder<Object, Object> toCacheBuilder() {
168    CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
169    if (initialCapacity != null) {
170      builder.initialCapacity(initialCapacity);
171    }
172    if (maximumSize != null) {
173      builder.maximumSize(maximumSize);
174    }
175    if (maximumWeight != null) {
176      builder.maximumWeight(maximumWeight);
177    }
178    if (concurrencyLevel != null) {
179      builder.concurrencyLevel(concurrencyLevel);
180    }
181    if (keyStrength != null) {
182      switch (keyStrength) {
183        case WEAK:
184          builder.weakKeys();
185          break;
186        default:
187          throw new AssertionError();
188      }
189    }
190    if (valueStrength != null) {
191      switch (valueStrength) {
192        case SOFT:
193          builder.softValues();
194          break;
195        case WEAK:
196          builder.weakValues();
197          break;
198        default:
199          throw new AssertionError();
200      }
201    }
202    if (recordStats != null && recordStats) {
203      builder.recordStats();
204    }
205    if (writeExpirationTimeUnit != null) {
206      builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
207    }
208    if (accessExpirationTimeUnit != null) {
209      builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
210    }
211    if (refreshTimeUnit != null) {
212      builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
213    }
214
215    return builder;
216  }
217
218  /**
219   * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order
220   * and form of this representation is not guaranteed, except that reparsing its output will
221   * produce a {@code CacheBuilderSpec} equal to this instance.
222   */
223  public String toParsableString() {
224    return specification;
225  }
226
227  /**
228   * Returns a string representation for this CacheBuilderSpec instance. The form of this
229   * representation is not guaranteed.
230   */
231  @Override
232  public String toString() {
233    return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
234  }
235
236  @Override
237  public int hashCode() {
238    return Objects.hashCode(
239        initialCapacity,
240        maximumSize,
241        maximumWeight,
242        concurrencyLevel,
243        keyStrength,
244        valueStrength,
245        recordStats,
246        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
247        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
248        durationInNanos(refreshDuration, refreshTimeUnit));
249  }
250
251  @Override
252  public boolean equals(@Nullable Object obj) {
253    if (this == obj) {
254      return true;
255    }
256    if (!(obj instanceof CacheBuilderSpec)) {
257      return false;
258    }
259    CacheBuilderSpec that = (CacheBuilderSpec) obj;
260    return Objects.equal(initialCapacity, that.initialCapacity)
261        && Objects.equal(maximumSize, that.maximumSize)
262        && Objects.equal(maximumWeight, that.maximumWeight)
263        && Objects.equal(concurrencyLevel, that.concurrencyLevel)
264        && Objects.equal(keyStrength, that.keyStrength)
265        && Objects.equal(valueStrength, that.valueStrength)
266        && Objects.equal(recordStats, that.recordStats)
267        && Objects.equal(
268            durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
269            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
270        && Objects.equal(
271            durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
272            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
273        && Objects.equal(
274            durationInNanos(refreshDuration, refreshTimeUnit),
275            durationInNanos(that.refreshDuration, that.refreshTimeUnit));
276  }
277
278  /**
279   * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses
280   * nanos to match CacheBuilder implementation.
281   */
282  private static @Nullable Long durationInNanos(long duration, @Nullable 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, @Nullable 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, @Nullable 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, @Nullable 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}