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.Nullable;
031
032/**
033 * A specification of a {@link CacheBuilder} configuration.
034 *
035 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which makes it
036 * especially useful for command-line configuration of a {@code CacheBuilder}.
037 *
038 * <p>The string syntax is a series of comma-separated keys or key-value pairs, each corresponding
039 * to a {@code CacheBuilder} method.
040 *
041 * <ul>
042 *   <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
043 *   <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
044 *   <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
045 *   <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
046 *   <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
047 *   <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
048 *   <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
049 *   <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
050 *   <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
051 *   <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
052 *   <li>{@code recordStats}: sets {@link CacheBuilder#recordStats}.
053 * </ul>
054 *
055 * <p>The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys will
056 * never be removed.
057 *
058 * <p>Durations are represented by an integer, followed by one of "d", "h", "m", or "s",
059 * representing days, hours, minutes, or seconds respectively. (There is currently no syntax to
060 * request expiration in milliseconds, microseconds, or nanoseconds.)
061 *
062 * <p>Whitespace before and after commas and equal signs is ignored. Keys may not be repeated; it is
063 * also illegal to use the following pairs of keys in a single value:
064 *
065 * <ul>
066 *   <li>{@code maximumSize} and {@code maximumWeight}
067 *   <li>{@code softValues} and {@code weakValues}
068 * </ul>
069 *
070 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods with
071 * non-value parameters. These must be configured in code.
072 *
073 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using {@link
074 * CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
075 *
076 * @author Adam Winer
077 * @since 12.0
078 */
079@SuppressWarnings("GoodTime") // lots of violations (nanosecond math)
080@GwtIncompatible
081public 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("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  @VisibleForTesting @Nullable Integer initialCapacity;
111  @VisibleForTesting @Nullable Long maximumSize;
112  @VisibleForTesting @Nullable Long maximumWeight;
113  @VisibleForTesting @Nullable Integer concurrencyLevel;
114  @VisibleForTesting @Nullable Strength keyStrength;
115  @VisibleForTesting @Nullable Strength valueStrength;
116  @VisibleForTesting @Nullable Boolean recordStats;
117  @VisibleForTesting long writeExpirationDuration;
118  @VisibleForTesting @Nullable TimeUnit writeExpirationTimeUnit;
119  @VisibleForTesting long accessExpirationDuration;
120  @VisibleForTesting @Nullable TimeUnit accessExpirationTimeUnit;
121  @VisibleForTesting long refreshDuration;
122  @VisibleForTesting @Nullable 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(@Nullable 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  private static @Nullable Long durationInNanos(long duration, @Nullable TimeUnit unit) {
282    return (unit == null) ? null : unit.toNanos(duration);
283  }
284
285  /** Base class for parsing integers. */
286  abstract static class IntegerParser implements ValueParser {
287    protected abstract void parseInteger(CacheBuilderSpec spec, int value);
288
289    @Override
290    public void parse(CacheBuilderSpec spec, String key, String value) {
291      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
292      try {
293        parseInteger(spec, Integer.parseInt(value));
294      } catch (NumberFormatException e) {
295        throw new IllegalArgumentException(
296            format("key %s value set to %s, must be integer", key, value), e);
297      }
298    }
299  }
300
301  /** Base class for parsing integers. */
302  abstract static class LongParser implements ValueParser {
303    protected abstract void parseLong(CacheBuilderSpec spec, long value);
304
305    @Override
306    public void parse(CacheBuilderSpec spec, String key, String value) {
307      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
308      try {
309        parseLong(spec, Long.parseLong(value));
310      } catch (NumberFormatException e) {
311        throw new IllegalArgumentException(
312            format("key %s value set to %s, must be integer", key, value), e);
313      }
314    }
315  }
316
317  /** Parse initialCapacity */
318  static class InitialCapacityParser extends IntegerParser {
319    @Override
320    protected void parseInteger(CacheBuilderSpec spec, int value) {
321      checkArgument(
322          spec.initialCapacity == null,
323          "initial capacity was already set to ",
324          spec.initialCapacity);
325      spec.initialCapacity = value;
326    }
327  }
328
329  /** Parse maximumSize */
330  static class MaximumSizeParser extends LongParser {
331    @Override
332    protected void parseLong(CacheBuilderSpec spec, long value) {
333      checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize);
334      checkArgument(
335          spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight);
336      spec.maximumSize = value;
337    }
338  }
339
340  /** Parse maximumWeight */
341  static class MaximumWeightParser extends LongParser {
342    @Override
343    protected void parseLong(CacheBuilderSpec spec, long value) {
344      checkArgument(
345          spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight);
346      checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize);
347      spec.maximumWeight = value;
348    }
349  }
350
351  /** Parse concurrencyLevel */
352  static class ConcurrencyLevelParser extends IntegerParser {
353    @Override
354    protected void parseInteger(CacheBuilderSpec spec, int value) {
355      checkArgument(
356          spec.concurrencyLevel == null,
357          "concurrency level was already set to ",
358          spec.concurrencyLevel);
359      spec.concurrencyLevel = value;
360    }
361  }
362
363  /** Parse weakKeys */
364  static class KeyStrengthParser implements ValueParser {
365    private final Strength strength;
366
367    public KeyStrengthParser(Strength strength) {
368      this.strength = strength;
369    }
370
371    @Override
372    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
373      checkArgument(value == null, "key %s does not take values", key);
374      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
375      spec.keyStrength = strength;
376    }
377  }
378
379  /** Parse weakValues and softValues */
380  static class ValueStrengthParser implements ValueParser {
381    private final Strength strength;
382
383    public ValueStrengthParser(Strength strength) {
384      this.strength = strength;
385    }
386
387    @Override
388    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
389      checkArgument(value == null, "key %s does not take values", key);
390      checkArgument(
391          spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength);
392
393      spec.valueStrength = strength;
394    }
395  }
396
397  /** Parse recordStats */
398  static class RecordStatsParser implements ValueParser {
399
400    @Override
401    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
402      checkArgument(value == null, "recordStats does not take values");
403      checkArgument(spec.recordStats == null, "recordStats already set");
404      spec.recordStats = true;
405    }
406  }
407
408  /** Base class for parsing times with durations */
409  abstract static class DurationParser implements ValueParser {
410    protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit);
411
412    @Override
413    public void parse(CacheBuilderSpec spec, String key, String value) {
414      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
415      try {
416        char lastChar = value.charAt(value.length() - 1);
417        TimeUnit timeUnit;
418        switch (lastChar) {
419          case 'd':
420            timeUnit = TimeUnit.DAYS;
421            break;
422          case 'h':
423            timeUnit = TimeUnit.HOURS;
424            break;
425          case 'm':
426            timeUnit = TimeUnit.MINUTES;
427            break;
428          case 's':
429            timeUnit = TimeUnit.SECONDS;
430            break;
431          default:
432            throw new IllegalArgumentException(
433                format(
434                    "key %s invalid format.  was %s, must end with one of [dDhHmMsS]", key, value));
435        }
436
437        long duration = Long.parseLong(value.substring(0, value.length() - 1));
438        parseDuration(spec, duration, timeUnit);
439      } catch (NumberFormatException e) {
440        throw new IllegalArgumentException(
441            format("key %s value set to %s, must be integer", key, value));
442      }
443    }
444  }
445
446  /** Parse expireAfterAccess */
447  static class AccessDurationParser extends DurationParser {
448    @Override
449    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
450      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
451      spec.accessExpirationDuration = duration;
452      spec.accessExpirationTimeUnit = unit;
453    }
454  }
455
456  /** Parse expireAfterWrite */
457  static class WriteDurationParser extends DurationParser {
458    @Override
459    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
460      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
461      spec.writeExpirationDuration = duration;
462      spec.writeExpirationTimeUnit = unit;
463    }
464  }
465
466  /** Parse refreshAfterWrite */
467  static class RefreshDurationParser extends DurationParser {
468    @Override
469    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
470      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
471      spec.refreshDuration = duration;
472      spec.refreshTimeUnit = unit;
473    }
474  }
475
476  private static String format(String format, Object... args) {
477    return String.format(Locale.ROOT, format, args);
478  }
479}