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