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