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