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