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