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