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          .build();
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 ",
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(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize);
342      checkArgument(
343          spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight);
344      spec.maximumSize = value;
345    }
346  }
347
348  /** Parse maximumWeight */
349  static class MaximumWeightParser extends LongParser {
350    @Override
351    protected void parseLong(CacheBuilderSpec spec, long value) {
352      checkArgument(
353          spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight);
354      checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize);
355      spec.maximumWeight = value;
356    }
357  }
358
359  /** Parse concurrencyLevel */
360  static class ConcurrencyLevelParser extends IntegerParser {
361    @Override
362    protected void parseInteger(CacheBuilderSpec spec, int value) {
363      checkArgument(
364          spec.concurrencyLevel == null,
365          "concurrency level was already set to ",
366          spec.concurrencyLevel);
367      spec.concurrencyLevel = value;
368    }
369  }
370
371  /** Parse weakKeys */
372  static class KeyStrengthParser implements ValueParser {
373    private final Strength strength;
374
375    public KeyStrengthParser(Strength strength) {
376      this.strength = strength;
377    }
378
379    @Override
380    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
381      checkArgument(value == null, "key %s does not take values", key);
382      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
383      spec.keyStrength = strength;
384    }
385  }
386
387  /** Parse weakValues and softValues */
388  static class ValueStrengthParser implements ValueParser {
389    private final Strength strength;
390
391    public ValueStrengthParser(Strength strength) {
392      this.strength = strength;
393    }
394
395    @Override
396    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
397      checkArgument(value == null, "key %s does not take values", key);
398      checkArgument(
399          spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength);
400
401      spec.valueStrength = strength;
402    }
403  }
404
405  /** Parse recordStats */
406  static class RecordStatsParser implements ValueParser {
407
408    @Override
409    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
410      checkArgument(value == null, "recordStats does not take values");
411      checkArgument(spec.recordStats == null, "recordStats already set");
412      spec.recordStats = true;
413    }
414  }
415
416  /** Base class for parsing times with durations */
417  abstract static class DurationParser implements ValueParser {
418    protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit);
419
420    @Override
421    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
422      if (isNullOrEmpty(value)) {
423        throw new IllegalArgumentException("value of key " + key + " omitted");
424      }
425      try {
426        char lastChar = value.charAt(value.length() - 1);
427        TimeUnit timeUnit;
428        switch (lastChar) {
429          case 'd':
430            timeUnit = TimeUnit.DAYS;
431            break;
432          case 'h':
433            timeUnit = TimeUnit.HOURS;
434            break;
435          case 'm':
436            timeUnit = TimeUnit.MINUTES;
437            break;
438          case 's':
439            timeUnit = TimeUnit.SECONDS;
440            break;
441          default:
442            throw new IllegalArgumentException(
443                format("key %s invalid unit: was %s, must end with one of [dhms]", key, value));
444        }
445
446        long duration = Long.parseLong(value.substring(0, value.length() - 1));
447        parseDuration(spec, duration, timeUnit);
448      } catch (NumberFormatException e) {
449        throw new IllegalArgumentException(
450            format("key %s value set to %s, must be integer", key, value));
451      }
452    }
453  }
454
455  /** Parse expireAfterAccess */
456  static class AccessDurationParser extends DurationParser {
457    @Override
458    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
459      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
460      spec.accessExpirationDuration = duration;
461      spec.accessExpirationTimeUnit = unit;
462    }
463  }
464
465  /** Parse expireAfterWrite */
466  static class WriteDurationParser extends DurationParser {
467    @Override
468    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
469      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
470      spec.writeExpirationDuration = duration;
471      spec.writeExpirationTimeUnit = unit;
472    }
473  }
474
475  /** Parse refreshAfterWrite */
476  static class RefreshDurationParser extends DurationParser {
477    @Override
478    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
479      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
480      spec.refreshDuration = duration;
481      spec.refreshTimeUnit = unit;
482    }
483  }
484
485  private static String format(String format, Object... args) {
486    return String.format(Locale.ROOT, format, args);
487  }
488}