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        case SOFT:
197        case STRONG:
198          throw new AssertionError();
199      }
200    }
201    if (valueStrength != null) {
202      switch (valueStrength) {
203        case SOFT:
204          builder.softValues();
205          break;
206        case WEAK:
207          builder.weakValues();
208          break;
209        case STRONG:
210          throw new AssertionError();
211      }
212    }
213    if (recordStats != null && recordStats) {
214      builder.recordStats();
215    }
216    if (writeExpirationTimeUnit != null) {
217      builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
218    }
219    if (accessExpirationTimeUnit != null) {
220      builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
221    }
222    if (refreshTimeUnit != null) {
223      builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
224    }
225
226    return builder;
227  }
228
229  /**
230   * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order
231   * and form of this representation is not guaranteed, except that reparsing its output will
232   * produce a {@code CacheBuilderSpec} equal to this instance.
233   */
234  public String toParsableString() {
235    return specification;
236  }
237
238  /**
239   * Returns a string representation for this CacheBuilderSpec instance. The form of this
240   * representation is not guaranteed.
241   */
242  @Override
243  public String toString() {
244    return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
245  }
246
247  @Override
248  public int hashCode() {
249    return Objects.hash(
250        initialCapacity,
251        maximumSize,
252        maximumWeight,
253        concurrencyLevel,
254        keyStrength,
255        valueStrength,
256        recordStats,
257        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
258        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
259        durationInNanos(refreshDuration, refreshTimeUnit));
260  }
261
262  @Override
263  public boolean equals(@Nullable Object obj) {
264    if (this == obj) {
265      return true;
266    }
267    if (!(obj instanceof CacheBuilderSpec)) {
268      return false;
269    }
270    CacheBuilderSpec that = (CacheBuilderSpec) obj;
271    return Objects.equals(initialCapacity, that.initialCapacity)
272        && Objects.equals(maximumSize, that.maximumSize)
273        && Objects.equals(maximumWeight, that.maximumWeight)
274        && Objects.equals(concurrencyLevel, that.concurrencyLevel)
275        && Objects.equals(keyStrength, that.keyStrength)
276        && Objects.equals(valueStrength, that.valueStrength)
277        && Objects.equals(recordStats, that.recordStats)
278        && Objects.equals(
279            durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
280            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
281        && Objects.equals(
282            durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
283            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
284        && Objects.equals(
285            durationInNanos(refreshDuration, refreshTimeUnit),
286            durationInNanos(that.refreshDuration, that.refreshTimeUnit));
287  }
288
289  /**
290   * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses
291   * nanos to match CacheBuilder implementation.
292   */
293  private static @Nullable Long durationInNanos(long duration, @Nullable TimeUnit unit) {
294    return (unit == null) ? null : unit.toNanos(duration);
295  }
296
297  /** Base class for parsing integers. */
298  private abstract static class IntegerParser implements ValueParser {
299    abstract void writeToSpec(CacheBuilderSpec spec, int value);
300
301    @Override
302    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
303      if (isNullOrEmpty(value)) {
304        throw new IllegalArgumentException("value of key " + key + " omitted");
305      }
306      try {
307        writeToSpec(spec, parseInt(value));
308      } catch (NumberFormatException e) {
309        throw new IllegalArgumentException(
310            format("key %s value set to %s, must be integer", key, value), e);
311      }
312    }
313  }
314
315  /** Base class for parsing integers. */
316  private abstract static class LongParser implements ValueParser {
317    abstract void writeToSpec(CacheBuilderSpec spec, long value);
318
319    @Override
320    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
321      if (isNullOrEmpty(value)) {
322        throw new IllegalArgumentException("value of key " + key + " omitted");
323      }
324      try {
325        writeToSpec(spec, parseLong(value));
326      } catch (NumberFormatException e) {
327        throw new IllegalArgumentException(
328            format("key %s value set to %s, must be integer", key, value), e);
329      }
330    }
331  }
332
333  /** Parse initialCapacity */
334  private static final class InitialCapacityParser extends IntegerParser {
335    @Override
336    void writeToSpec(CacheBuilderSpec spec, int value) {
337      checkArgument(
338          spec.initialCapacity == null,
339          "initial capacity was already set to %s",
340          spec.initialCapacity);
341      spec.initialCapacity = value;
342    }
343  }
344
345  /** Parse maximumSize */
346  private static final class MaximumSizeParser extends LongParser {
347    @Override
348    void writeToSpec(CacheBuilderSpec spec, long value) {
349      checkArgument(
350          spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize);
351      checkArgument(
352          spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight);
353      spec.maximumSize = value;
354    }
355  }
356
357  /** Parse maximumWeight */
358  private static final class MaximumWeightParser extends LongParser {
359    @Override
360    void writeToSpec(CacheBuilderSpec spec, long value) {
361      checkArgument(
362          spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight);
363      checkArgument(
364          spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize);
365      spec.maximumWeight = value;
366    }
367  }
368
369  /** Parse concurrencyLevel */
370  private static final class ConcurrencyLevelParser extends IntegerParser {
371    @Override
372    void writeToSpec(CacheBuilderSpec spec, int value) {
373      checkArgument(
374          spec.concurrencyLevel == null,
375          "concurrency level was already set to %s",
376          spec.concurrencyLevel);
377      spec.concurrencyLevel = value;
378    }
379  }
380
381  /** Parse weakKeys */
382  private static final class KeyStrengthParser implements ValueParser {
383    private final Strength strength;
384
385    KeyStrengthParser(Strength strength) {
386      this.strength = strength;
387    }
388
389    @Override
390    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
391      checkArgument(value == null, "key %s does not take values", key);
392      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
393      spec.keyStrength = strength;
394    }
395  }
396
397  /** Parse weakValues and softValues */
398  private static final class ValueStrengthParser implements ValueParser {
399    private final Strength strength;
400
401    ValueStrengthParser(Strength strength) {
402      this.strength = strength;
403    }
404
405    @Override
406    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
407      checkArgument(value == null, "key %s does not take values", key);
408      checkArgument(
409          spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength);
410
411      spec.valueStrength = strength;
412    }
413  }
414
415  /** Parse recordStats */
416  private static final class RecordStatsParser implements ValueParser {
417
418    @Override
419    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
420      checkArgument(value == null, "recordStats does not take values");
421      checkArgument(spec.recordStats == null, "recordStats already set");
422      spec.recordStats = true;
423    }
424  }
425
426  /** Base class for parsing times with durations */
427  private abstract static class DurationParser implements ValueParser {
428    abstract void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit);
429
430    @Override
431    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
432      if (isNullOrEmpty(value)) {
433        throw new IllegalArgumentException("value of key " + key + " omitted");
434      }
435      try {
436        char lastChar = value.charAt(value.length() - 1);
437        TimeUnit timeUnit;
438        switch (lastChar) {
439          case 'd':
440            timeUnit = DAYS;
441            break;
442          case 'h':
443            timeUnit = HOURS;
444            break;
445          case 'm':
446            timeUnit = MINUTES;
447            break;
448          case 's':
449            timeUnit = SECONDS;
450            break;
451          default:
452            throw new IllegalArgumentException(
453                format("key %s invalid unit: was %s, must end with one of [dhms]", key, value));
454        }
455
456        long duration = parseLong(value.substring(0, value.length() - 1));
457        writeToSpec(spec, duration, timeUnit);
458      } catch (NumberFormatException e) {
459        throw new IllegalArgumentException(
460            format("key %s value set to %s, must be integer", key, value));
461      }
462    }
463  }
464
465  /** Parse expireAfterAccess */
466  private static final class AccessDurationParser extends DurationParser {
467    @Override
468    void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) {
469      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
470      spec.accessExpirationDuration = duration;
471      spec.accessExpirationTimeUnit = unit;
472    }
473  }
474
475  /** Parse expireAfterWrite */
476  private static final class WriteDurationParser extends DurationParser {
477    @Override
478    void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) {
479      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
480      spec.writeExpirationDuration = duration;
481      spec.writeExpirationTimeUnit = unit;
482    }
483  }
484
485  /** Parse refreshAfterWrite */
486  private static final class RefreshDurationParser extends DurationParser {
487    @Override
488    void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) {
489      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
490      spec.refreshDuration = duration;
491      spec.refreshTimeUnit = unit;
492    }
493  }
494
495  @FormatMethod
496  private static String format(String format, Object... args) {
497    return String.format(Locale.ROOT, format, args);
498  }
499}