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
087@ElementTypesAreNonnullByDefault
088public final class CacheBuilderSpec {
089  /** Parses a single value. */
090  private interface ValueParser {
091    void parse(CacheBuilderSpec spec, String key, @CheckForNull String value);
092  }
093
094  /** Splits each key-value pair. */
095  private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
096
097  /** Splits the key from the value. */
098  private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
099
100  /** Map of names to ValueParser. */
101  private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
102      ImmutableMap.<String, ValueParser>builder()
103          .put("initialCapacity", new InitialCapacityParser())
104          .put("maximumSize", new MaximumSizeParser())
105          .put("maximumWeight", new MaximumWeightParser())
106          .put("concurrencyLevel", new ConcurrencyLevelParser())
107          .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
108          .put("softValues", new ValueStrengthParser(Strength.SOFT))
109          .put("weakValues", new ValueStrengthParser(Strength.WEAK))
110          .put("recordStats", new RecordStatsParser())
111          .put("expireAfterAccess", new AccessDurationParser())
112          .put("expireAfterWrite", new WriteDurationParser())
113          .put("refreshAfterWrite", new RefreshDurationParser())
114          .put("refreshInterval", new RefreshDurationParser())
115          .buildOrThrow();
116
117  @VisibleForTesting @CheckForNull Integer initialCapacity;
118  @VisibleForTesting @CheckForNull Long maximumSize;
119  @VisibleForTesting @CheckForNull Long maximumWeight;
120  @VisibleForTesting @CheckForNull Integer concurrencyLevel;
121  @VisibleForTesting @CheckForNull Strength keyStrength;
122  @VisibleForTesting @CheckForNull Strength valueStrength;
123  @VisibleForTesting @CheckForNull Boolean recordStats;
124  @VisibleForTesting long writeExpirationDuration;
125  @VisibleForTesting @CheckForNull TimeUnit writeExpirationTimeUnit;
126  @VisibleForTesting long accessExpirationDuration;
127  @VisibleForTesting @CheckForNull TimeUnit accessExpirationTimeUnit;
128  @VisibleForTesting long refreshDuration;
129  @VisibleForTesting @CheckForNull TimeUnit refreshTimeUnit;
130  /** Specification; used for toParseableString(). */
131  private final String specification;
132
133  private CacheBuilderSpec(String specification) {
134    this.specification = specification;
135  }
136
137  /**
138   * Creates a CacheBuilderSpec from a string.
139   *
140   * @param cacheBuilderSpecification the string form
141   */
142  public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
143    CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
144    if (!cacheBuilderSpecification.isEmpty()) {
145      for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
146        List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
147        checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
148        checkArgument(
149            keyAndValue.size() <= 2,
150            "key-value pair %s with more than one equals sign",
151            keyValuePair);
152
153        // Find the ValueParser for the current key.
154        String key = keyAndValue.get(0);
155        ValueParser valueParser = VALUE_PARSERS.get(key);
156        checkArgument(valueParser != null, "unknown key %s", key);
157
158        String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
159        valueParser.parse(spec, key, value);
160      }
161    }
162
163    return spec;
164  }
165
166  /** Returns a CacheBuilderSpec that will prevent caching. */
167  public static CacheBuilderSpec disableCaching() {
168    // Maximum size of zero is one way to block caching
169    return CacheBuilderSpec.parse("maximumSize=0");
170  }
171
172  /** Returns a CacheBuilder configured according to this instance's specification. */
173  CacheBuilder<Object, Object> toCacheBuilder() {
174    CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
175    if (initialCapacity != null) {
176      builder.initialCapacity(initialCapacity);
177    }
178    if (maximumSize != null) {
179      builder.maximumSize(maximumSize);
180    }
181    if (maximumWeight != null) {
182      builder.maximumWeight(maximumWeight);
183    }
184    if (concurrencyLevel != null) {
185      builder.concurrencyLevel(concurrencyLevel);
186    }
187    if (keyStrength != null) {
188      switch (keyStrength) {
189        case WEAK:
190          builder.weakKeys();
191          break;
192        default:
193          throw new AssertionError();
194      }
195    }
196    if (valueStrength != null) {
197      switch (valueStrength) {
198        case SOFT:
199          builder.softValues();
200          break;
201        case WEAK:
202          builder.weakValues();
203          break;
204        default:
205          throw new AssertionError();
206      }
207    }
208    if (recordStats != null && recordStats) {
209      builder.recordStats();
210    }
211    if (writeExpirationTimeUnit != null) {
212      builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
213    }
214    if (accessExpirationTimeUnit != null) {
215      builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
216    }
217    if (refreshTimeUnit != null) {
218      builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
219    }
220
221    return builder;
222  }
223
224  /**
225   * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order
226   * and form of this representation is not guaranteed, except that reparsing its output will
227   * produce a {@code CacheBuilderSpec} equal to this instance.
228   */
229  public String toParsableString() {
230    return specification;
231  }
232
233  /**
234   * Returns a string representation for this CacheBuilderSpec instance. The form of this
235   * representation is not guaranteed.
236   */
237  @Override
238  public String toString() {
239    return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
240  }
241
242  @Override
243  public int hashCode() {
244    return Objects.hashCode(
245        initialCapacity,
246        maximumSize,
247        maximumWeight,
248        concurrencyLevel,
249        keyStrength,
250        valueStrength,
251        recordStats,
252        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
253        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
254        durationInNanos(refreshDuration, refreshTimeUnit));
255  }
256
257  @Override
258  public boolean equals(@CheckForNull Object obj) {
259    if (this == obj) {
260      return true;
261    }
262    if (!(obj instanceof CacheBuilderSpec)) {
263      return false;
264    }
265    CacheBuilderSpec that = (CacheBuilderSpec) obj;
266    return Objects.equal(initialCapacity, that.initialCapacity)
267        && Objects.equal(maximumSize, that.maximumSize)
268        && Objects.equal(maximumWeight, that.maximumWeight)
269        && Objects.equal(concurrencyLevel, that.concurrencyLevel)
270        && Objects.equal(keyStrength, that.keyStrength)
271        && Objects.equal(valueStrength, that.valueStrength)
272        && Objects.equal(recordStats, that.recordStats)
273        && Objects.equal(
274            durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
275            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
276        && Objects.equal(
277            durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
278            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
279        && Objects.equal(
280            durationInNanos(refreshDuration, refreshTimeUnit),
281            durationInNanos(that.refreshDuration, that.refreshTimeUnit));
282  }
283
284  /**
285   * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses
286   * nanos to match CacheBuilder implementation.
287   */
288  @CheckForNull
289  private static Long durationInNanos(long duration, @CheckForNull TimeUnit unit) {
290    return (unit == null) ? null : unit.toNanos(duration);
291  }
292
293  /** Base class for parsing integers. */
294  abstract static class IntegerParser implements ValueParser {
295    protected abstract void parseInteger(CacheBuilderSpec spec, int value);
296
297    @Override
298    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
299      if (isNullOrEmpty(value)) {
300        throw new IllegalArgumentException("value of key " + key + " omitted");
301      }
302      try {
303        parseInteger(spec, Integer.parseInt(value));
304      } catch (NumberFormatException e) {
305        throw new IllegalArgumentException(
306            format("key %s value set to %s, must be integer", key, value), e);
307      }
308    }
309  }
310
311  /** Base class for parsing integers. */
312  abstract static class LongParser implements ValueParser {
313    protected abstract void parseLong(CacheBuilderSpec spec, long value);
314
315    @Override
316    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
317      if (isNullOrEmpty(value)) {
318        throw new IllegalArgumentException("value of key " + key + " omitted");
319      }
320      try {
321        parseLong(spec, Long.parseLong(value));
322      } catch (NumberFormatException e) {
323        throw new IllegalArgumentException(
324            format("key %s value set to %s, must be integer", key, value), e);
325      }
326    }
327  }
328
329  /** Parse initialCapacity */
330  static class InitialCapacityParser extends IntegerParser {
331    @Override
332    protected void parseInteger(CacheBuilderSpec spec, int value) {
333      checkArgument(
334          spec.initialCapacity == null,
335          "initial capacity was already set to %s",
336          spec.initialCapacity);
337      spec.initialCapacity = value;
338    }
339  }
340
341  /** Parse maximumSize */
342  static class MaximumSizeParser extends LongParser {
343    @Override
344    protected void parseLong(CacheBuilderSpec spec, long value) {
345      checkArgument(
346          spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize);
347      checkArgument(
348          spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight);
349      spec.maximumSize = value;
350    }
351  }
352
353  /** Parse maximumWeight */
354  static class MaximumWeightParser extends LongParser {
355    @Override
356    protected void parseLong(CacheBuilderSpec spec, long value) {
357      checkArgument(
358          spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight);
359      checkArgument(
360          spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize);
361      spec.maximumWeight = value;
362    }
363  }
364
365  /** Parse concurrencyLevel */
366  static class ConcurrencyLevelParser extends IntegerParser {
367    @Override
368    protected void parseInteger(CacheBuilderSpec spec, int value) {
369      checkArgument(
370          spec.concurrencyLevel == null,
371          "concurrency level was already set to %s",
372          spec.concurrencyLevel);
373      spec.concurrencyLevel = value;
374    }
375  }
376
377  /** Parse weakKeys */
378  static class KeyStrengthParser implements ValueParser {
379    private final Strength strength;
380
381    public KeyStrengthParser(Strength strength) {
382      this.strength = strength;
383    }
384
385    @Override
386    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
387      checkArgument(value == null, "key %s does not take values", key);
388      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
389      spec.keyStrength = strength;
390    }
391  }
392
393  /** Parse weakValues and softValues */
394  static class ValueStrengthParser implements ValueParser {
395    private final Strength strength;
396
397    public ValueStrengthParser(Strength strength) {
398      this.strength = strength;
399    }
400
401    @Override
402    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
403      checkArgument(value == null, "key %s does not take values", key);
404      checkArgument(
405          spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength);
406
407      spec.valueStrength = strength;
408    }
409  }
410
411  /** Parse recordStats */
412  static class RecordStatsParser implements ValueParser {
413
414    @Override
415    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
416      checkArgument(value == null, "recordStats does not take values");
417      checkArgument(spec.recordStats == null, "recordStats already set");
418      spec.recordStats = true;
419    }
420  }
421
422  /** Base class for parsing times with durations */
423  abstract static class DurationParser implements ValueParser {
424    protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit);
425
426    @Override
427    public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) {
428      if (isNullOrEmpty(value)) {
429        throw new IllegalArgumentException("value of key " + key + " omitted");
430      }
431      try {
432        char lastChar = value.charAt(value.length() - 1);
433        TimeUnit timeUnit;
434        switch (lastChar) {
435          case 'd':
436            timeUnit = DAYS;
437            break;
438          case 'h':
439            timeUnit = HOURS;
440            break;
441          case 'm':
442            timeUnit = MINUTES;
443            break;
444          case 's':
445            timeUnit = SECONDS;
446            break;
447          default:
448            throw new IllegalArgumentException(
449                format("key %s invalid unit: was %s, must end with one of [dhms]", key, value));
450        }
451
452        long duration = Long.parseLong(value.substring(0, value.length() - 1));
453        parseDuration(spec, duration, timeUnit);
454      } catch (NumberFormatException e) {
455        throw new IllegalArgumentException(
456            format("key %s value set to %s, must be integer", key, value));
457      }
458    }
459  }
460
461  /** Parse expireAfterAccess */
462  static class AccessDurationParser extends DurationParser {
463    @Override
464    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
465      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
466      spec.accessExpirationDuration = duration;
467      spec.accessExpirationTimeUnit = unit;
468    }
469  }
470
471  /** Parse expireAfterWrite */
472  static class WriteDurationParser extends DurationParser {
473    @Override
474    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
475      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
476      spec.writeExpirationDuration = duration;
477      spec.writeExpirationTimeUnit = unit;
478    }
479  }
480
481  /** Parse refreshAfterWrite */
482  static class RefreshDurationParser extends DurationParser {
483    @Override
484    protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
485      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
486      spec.refreshDuration = duration;
487      spec.refreshTimeUnit = unit;
488    }
489  }
490
491  private static String format(String format, Object... args) {
492    return String.format(Locale.ROOT, format, args);
493  }
494}