001/*
002 * Copyright (C) 2011 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.cache;
018
019import static com.google.common.base.Preconditions.checkArgument;
020
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;
028
029import java.util.List;
030import java.util.Locale;
031import java.util.concurrent.TimeUnit;
032
033import javax.annotation.Nullable;
034
035/**
036 * A specification of a {@link CacheBuilder} configuration.
037 *
038 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
039 * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
040 *
041 * <p>The string syntax is a series of comma-separated keys or key-value pairs,
042 * each corresponding to a {@code CacheBuilder} method.
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
058 * will never be removed.
059 *
060 * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
061 * or "s", representing days, hours, minutes, or seconds respectively.  (There
062 * is currently no syntax to request expiration in milliseconds, microseconds,
063 * or nanoseconds.)
064 *
065 * <p>Whitespace before and after commas and equal signs is ignored.  Keys may
066 * not be repeated;  it is also illegal to use the following pairs of keys in
067 * a single value:
068 * <ul>
069 * <li>{@code maximumSize} and {@code maximumWeight}
070 * <li>{@code softValues} and {@code weakValues}
071 * </ul>
072 *
073 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
074 * with non-value parameters.  These must be configured in code.
075 *
076 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
077 * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
078 *
079 * @author Adam Winer
080 * @since 12.0
081 */
082public final class CacheBuilderSpec {
083  /** Parses a single value. */
084  private interface ValueParser {
085    void parse(CacheBuilderSpec spec, String key, @Nullable String value);
086  }
087
088  /** Splits each key-value pair. */
089  private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
090
091  /** Splits the key from the value. */
092  private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
093
094  /** Map of names to ValueParser. */
095  private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
096      ImmutableMap.<String, ValueParser>builder()
097          .put("initialCapacity", new InitialCapacityParser())
098          .put("maximumSize", new MaximumSizeParser())
099          .put("maximumWeight", new MaximumWeightParser())
100          .put("concurrencyLevel", new ConcurrencyLevelParser())
101          .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
102          .put("softValues", new ValueStrengthParser(Strength.SOFT))
103          .put("weakValues", new ValueStrengthParser(Strength.WEAK))
104          .put("recordStats", new RecordStatsParser())
105          .put("expireAfterAccess", new AccessDurationParser())
106          .put("expireAfterWrite", new WriteDurationParser())
107          .put("refreshAfterWrite", new RefreshDurationParser())
108          .put("refreshInterval", new RefreshDurationParser())
109          .build();
110
111  @VisibleForTesting Integer initialCapacity;
112  @VisibleForTesting Long maximumSize;
113  @VisibleForTesting Long maximumWeight;
114  @VisibleForTesting Integer concurrencyLevel;
115  @VisibleForTesting Strength keyStrength;
116  @VisibleForTesting Strength valueStrength;
117  @VisibleForTesting Boolean recordStats;
118  @VisibleForTesting long writeExpirationDuration;
119  @VisibleForTesting TimeUnit writeExpirationTimeUnit;
120  @VisibleForTesting long accessExpirationDuration;
121  @VisibleForTesting TimeUnit accessExpirationTimeUnit;
122  @VisibleForTesting long refreshDuration;
123  @VisibleForTesting TimeUnit refreshTimeUnit;
124  /** Specification;  used for toParseableString(). */
125  private final String specification;
126
127  private CacheBuilderSpec(String specification) {
128    this.specification = specification;
129  }
130
131  /**
132   * Creates a CacheBuilderSpec from a string.
133   *
134   * @param cacheBuilderSpecification the string form
135   */
136  public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
137    CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
138    if (!cacheBuilderSpecification.isEmpty()) {
139      for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
140        List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
141        checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
142        checkArgument(keyAndValue.size() <= 2,
143            "key-value pair %s with more than one equals sign", 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  /**
159   * Returns a CacheBuilderSpec that will prevent caching.
160   */
161  public static CacheBuilderSpec disableCaching() {
162    // Maximum size of zero is one way to block caching
163    return CacheBuilderSpec.parse("maximumSize=0");
164  }
165
166  /**
167   * Returns a CacheBuilder configured according to this instance's specification.
168   */
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
222   * {@code CacheBuilderSpec}.  The order and form of this representation is
223   * not guaranteed, except that reparsing its output will produce
224   * a {@code CacheBuilderSpec} equal to this instance.
225   */
226  public String toParsableString() {
227    return specification;
228  }
229
230  /**
231   * Returns a string representation for this CacheBuilderSpec instance.
232   * The form of this representation is not guaranteed.
233   */
234  @Override
235  public String toString() {
236    return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
237  }
238
239  @Override
240  public int hashCode() {
241    return Objects.hashCode(
242        initialCapacity,
243        maximumSize,
244        maximumWeight,
245        concurrencyLevel,
246        keyStrength,
247        valueStrength,
248        recordStats,
249        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
250        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
251        durationInNanos(refreshDuration, refreshTimeUnit));
252  }
253
254  @Override
255  public boolean equals(@Nullable Object obj) {
256    if (this == obj) {
257      return true;
258    }
259    if (!(obj instanceof CacheBuilderSpec)) {
260      return false;
261    }
262    CacheBuilderSpec that = (CacheBuilderSpec) obj;
263    return Objects.equal(initialCapacity, that.initialCapacity)
264        && Objects.equal(maximumSize, that.maximumSize)
265        && Objects.equal(maximumWeight, that.maximumWeight)
266        && Objects.equal(concurrencyLevel, that.concurrencyLevel)
267        && Objects.equal(keyStrength, that.keyStrength)
268        && Objects.equal(valueStrength, that.valueStrength)
269        && Objects.equal(recordStats, that.recordStats)
270        && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
271            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
272        && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
273            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
274        && Objects.equal(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.
280   * Uses nanos to match CacheBuilder implementation.
281   */
282  @Nullable private static Long durationInNanos(long duration, @Nullable TimeUnit unit) {
283    return (unit == null) ? null : unit.toNanos(duration);
284  }
285
286  /** Base class for parsing integers. */
287  abstract static class IntegerParser implements ValueParser {
288    protected abstract void parseInteger(CacheBuilderSpec spec, int value);
289
290    @Override
291    public void parse(CacheBuilderSpec spec, String key, String value) {
292      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
293      try {
294        parseInteger(spec, Integer.parseInt(value));
295      } catch (NumberFormatException e) {
296        throw new IllegalArgumentException(
297            format("key %s value set to %s, must be integer", key, value), e);
298      }
299    }
300  }
301
302  /** Base class for parsing integers. */
303  abstract static class LongParser implements ValueParser {
304    protected abstract void parseLong(CacheBuilderSpec spec, long value);
305
306    @Override
307    public void parse(CacheBuilderSpec spec, String key, String value) {
308      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
309      try {
310        parseLong(spec, Long.parseLong(value));
311      } catch (NumberFormatException e) {
312        throw new IllegalArgumentException(
313            format("key %s value set to %s, must be integer", key, value), e);
314      }
315    }
316  }
317
318  /** Parse initialCapacity */
319  static class InitialCapacityParser extends IntegerParser {
320    @Override
321    protected void parseInteger(CacheBuilderSpec spec, int value) {
322      checkArgument(spec.initialCapacity == null,
323          "initial capacity was already set to ", spec.initialCapacity);
324      spec.initialCapacity = value;
325    }
326  }
327
328  /** Parse maximumSize */
329  static class MaximumSizeParser extends LongParser {
330    @Override
331    protected void parseLong(CacheBuilderSpec spec, long value) {
332      checkArgument(spec.maximumSize == null,
333          "maximum size was already set to ", spec.maximumSize);
334      checkArgument(spec.maximumWeight == null,
335          "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(spec.maximumWeight == null,
345          "maximum weight was already set to ", spec.maximumWeight);
346      checkArgument(spec.maximumSize == null,
347          "maximum size was already set to ", spec.maximumSize);
348      spec.maximumWeight = value;
349    }
350  }
351
352  /** Parse concurrencyLevel */
353  static class ConcurrencyLevelParser extends IntegerParser {
354    @Override
355    protected void parseInteger(CacheBuilderSpec spec, int value) {
356      checkArgument(spec.concurrencyLevel == null,
357          "concurrency level was already set to ", spec.concurrencyLevel);
358      spec.concurrencyLevel = value;
359    }
360  }
361
362  /** Parse weakKeys */
363  static class KeyStrengthParser implements ValueParser {
364    private final Strength strength;
365
366    public KeyStrengthParser(Strength strength) {
367      this.strength = strength;
368    }
369
370    @Override
371    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
372      checkArgument(value == null, "key %s does not take values", key);
373      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
374      spec.keyStrength = strength;
375    }
376  }
377
378  /** Parse weakValues and softValues */
379  static class ValueStrengthParser implements ValueParser {
380    private final Strength strength;
381
382    public ValueStrengthParser(Strength strength) {
383      this.strength = strength;
384    }
385
386    @Override
387    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
388      checkArgument(value == null, "key %s does not take values", key);
389      checkArgument(spec.valueStrength == null,
390        "%s was already set to %s", key, spec.valueStrength);
391
392      spec.valueStrength = strength;
393    }
394  }
395
396  /** Parse recordStats */
397  static class RecordStatsParser implements ValueParser {
398
399    @Override
400    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
401      checkArgument(value == null, "recordStats does not take values");
402      checkArgument(spec.recordStats == null, "recordStats already set");
403      spec.recordStats = true;
404    }
405  }
406
407  /** Base class for parsing times with durations */
408  abstract static class DurationParser implements ValueParser {
409    protected abstract void parseDuration(
410        CacheBuilderSpec spec,
411        long duration,
412        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("key %s invalid format.  was %s, must end with one of [dDhHmMsS]",
436                    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 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
451      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
452      spec.accessExpirationDuration = duration;
453      spec.accessExpirationTimeUnit = unit;
454    }
455  }
456
457  /** Parse expireAfterWrite */
458  static class WriteDurationParser extends DurationParser {
459    @Override 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 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
469      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
470      spec.refreshDuration = duration;
471      spec.refreshTimeUnit = unit;
472    }
473  }
474
475  private static String format(String format, Object... args) {
476    return String.format(Locale.ROOT, format, args);
477  }
478}