1/*
2 * Copyright (C) 2011 The Guava Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.common.cache;
18
19import static com.google.common.base.Preconditions.checkArgument;
20
21import com.google.common.annotations.Beta;
22import com.google.common.annotations.VisibleForTesting;
23import com.google.common.base.MoreObjects;
24import com.google.common.base.Objects;
25import com.google.common.base.Splitter;
26import com.google.common.cache.LocalCache.Strength;
27import com.google.common.collect.ImmutableList;
28import com.google.common.collect.ImmutableMap;
29
30import java.util.List;
31import java.util.concurrent.TimeUnit;
32
33import javax.annotation.Nullable;
34
35/**
36 * A specification of a {@link CacheBuilder} configuration.
37 *
38 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
39 * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
40 *
41 * <p>The string syntax is a series of comma-separated keys or key-value pairs,
42 * each corresponding to a {@code CacheBuilder} method.
43 * <ul>
44 * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
45 * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
46 * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
47 * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
48 * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
49 * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
50 * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
51 * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
52 * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
53 * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
54 * <li>{@code recordStats}: sets {@link CacheBuilder#recordStats}.
55 * </ul>
56 *
57 * <p>The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys
58 * will never be removed.
59 *
60 * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
61 * or "s", representing days, hours, minutes, or seconds respectively.  (There
62 * is currently no syntax to request expiration in milliseconds, microseconds,
63 * or nanoseconds.)
64 *
65 * <p>Whitespace before and after commas and equal signs is ignored.  Keys may
66 * not be repeated;  it is also illegal to use the following pairs of keys in
67 * a single value:
68 * <ul>
69 * <li>{@code maximumSize} and {@code maximumWeight}
70 * <li>{@code softValues} and {@code weakValues}
71 * </ul>
72 *
73 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
74 * with non-value parameters.  These must be configured in code.
75 *
76 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
77 * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
78 *
79 * @author Adam Winer
80 * @since 12.0
81 */
82@Beta
83public final class CacheBuilderSpec {
84  /** Parses a single value. */
85  private interface ValueParser {
86    void parse(CacheBuilderSpec spec, String key, @Nullable String value);
87  }
88
89  /** Splits each key-value pair. */
90  private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
91
92  /** Splits the key from the value. */
93  private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
94
95  /** Map of names to ValueParser. */
96  private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
97      ImmutableMap.<String, ValueParser>builder()
98          .put("initialCapacity", new InitialCapacityParser())
99          .put("maximumSize", new MaximumSizeParser())
100          .put("maximumWeight", new MaximumWeightParser())
101          .put("concurrencyLevel", new ConcurrencyLevelParser())
102          .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
103          .put("softValues", new ValueStrengthParser(Strength.SOFT))
104          .put("weakValues", new ValueStrengthParser(Strength.WEAK))
105          .put("recordStats", new RecordStatsParser())
106          .put("expireAfterAccess", new AccessDurationParser())
107          .put("expireAfterWrite", new WriteDurationParser())
108          .put("refreshAfterWrite", new RefreshDurationParser())
109          .put("refreshInterval", new RefreshDurationParser())
110          .build();
111
112  @VisibleForTesting Integer initialCapacity;
113  @VisibleForTesting Long maximumSize;
114  @VisibleForTesting Long maximumWeight;
115  @VisibleForTesting Integer concurrencyLevel;
116  @VisibleForTesting Strength keyStrength;
117  @VisibleForTesting Strength valueStrength;
118  @VisibleForTesting Boolean recordStats;
119  @VisibleForTesting long writeExpirationDuration;
120  @VisibleForTesting TimeUnit writeExpirationTimeUnit;
121  @VisibleForTesting long accessExpirationDuration;
122  @VisibleForTesting TimeUnit accessExpirationTimeUnit;
123  @VisibleForTesting long refreshDuration;
124  @VisibleForTesting TimeUnit refreshTimeUnit;
125  /** Specification;  used for toParseableString(). */
126  private final String specification;
127
128  private CacheBuilderSpec(String specification) {
129    this.specification = specification;
130  }
131
132  /**
133   * Creates a CacheBuilderSpec from a string.
134   *
135   * @param cacheBuilderSpecification the string form
136   */
137  public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
138    CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
139    if (!cacheBuilderSpecification.isEmpty()) {
140      for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
141        List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
142        checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
143        checkArgument(keyAndValue.size() <= 2,
144            "key-value pair %s with more than one equals sign", keyValuePair);
145
146        // Find the ValueParser for the current key.
147        String key = keyAndValue.get(0);
148        ValueParser valueParser = VALUE_PARSERS.get(key);
149        checkArgument(valueParser != null, "unknown key %s", key);
150
151        String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
152        valueParser.parse(spec, key, value);
153      }
154    }
155
156    return spec;
157  }
158
159  /**
160   * Returns a CacheBuilderSpec that will prevent caching.
161   */
162  public static CacheBuilderSpec disableCaching() {
163    // Maximum size of zero is one way to block caching
164    return CacheBuilderSpec.parse("maximumSize=0");
165  }
166
167  /**
168   * Returns a CacheBuilder configured according to this instance's specification.
169   */
170  CacheBuilder<Object, Object> toCacheBuilder() {
171    CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
172    if (initialCapacity != null) {
173      builder.initialCapacity(initialCapacity);
174    }
175    if (maximumSize != null) {
176      builder.maximumSize(maximumSize);
177    }
178    if (maximumWeight != null) {
179      builder.maximumWeight(maximumWeight);
180    }
181    if (concurrencyLevel != null) {
182      builder.concurrencyLevel(concurrencyLevel);
183    }
184    if (keyStrength != null) {
185      switch (keyStrength) {
186        case WEAK:
187          builder.weakKeys();
188          break;
189        default:
190          throw new AssertionError();
191      }
192    }
193    if (valueStrength != null) {
194      switch (valueStrength) {
195        case SOFT:
196          builder.softValues();
197          break;
198        case WEAK:
199          builder.weakValues();
200          break;
201        default:
202          throw new AssertionError();
203      }
204    }
205    if (recordStats != null && recordStats) {
206      builder.recordStats();
207    }
208    if (writeExpirationTimeUnit != null) {
209      builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
210    }
211    if (accessExpirationTimeUnit != null) {
212      builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
213    }
214    if (refreshTimeUnit != null) {
215      builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
216    }
217
218    return builder;
219  }
220
221  /**
222   * Returns a string that can be used to parse an equivalent
223   * {@code CacheBuilderSpec}.  The order and form of this representation is
224   * not guaranteed, except that reparsing its output will produce
225   * a {@code CacheBuilderSpec} equal to this instance.
226   */
227  public String toParsableString() {
228    return specification;
229  }
230
231  /**
232   * Returns a string representation for this CacheBuilderSpec instance.
233   * The form of this representation is not guaranteed.
234   */
235  @Override
236  public String toString() {
237    return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
238  }
239
240  @Override
241  public int hashCode() {
242    return Objects.hashCode(
243        initialCapacity,
244        maximumSize,
245        maximumWeight,
246        concurrencyLevel,
247        keyStrength,
248        valueStrength,
249        recordStats,
250        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
251        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
252        durationInNanos(refreshDuration, refreshTimeUnit));
253  }
254
255  @Override
256  public boolean equals(@Nullable Object obj) {
257    if (this == obj) {
258      return true;
259    }
260    if (!(obj instanceof CacheBuilderSpec)) {
261      return false;
262    }
263    CacheBuilderSpec that = (CacheBuilderSpec) obj;
264    return Objects.equal(initialCapacity, that.initialCapacity)
265        && Objects.equal(maximumSize, that.maximumSize)
266        && Objects.equal(maximumWeight, that.maximumWeight)
267        && Objects.equal(concurrencyLevel, that.concurrencyLevel)
268        && Objects.equal(keyStrength, that.keyStrength)
269        && Objects.equal(valueStrength, that.valueStrength)
270        && Objects.equal(recordStats, that.recordStats)
271        && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
272            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
273        && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
274            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
275        && Objects.equal(durationInNanos(refreshDuration, refreshTimeUnit),
276            durationInNanos(that.refreshDuration, that.refreshTimeUnit));
277  }
278
279  /**
280   * Converts an expiration duration/unit pair into a single Long for hashing and equality.
281   * Uses nanos to match CacheBuilder implementation.
282   */
283  @Nullable 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            String.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            String.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(spec.initialCapacity == null,
324          "initial capacity was already set to ", 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,
334          "maximum size was already set to ", spec.maximumSize);
335      checkArgument(spec.maximumWeight == null,
336          "maximum weight was already set to ", spec.maximumWeight);
337      spec.maximumSize = value;
338    }
339  }
340
341  /** Parse maximumWeight */
342  static class MaximumWeightParser extends LongParser {
343    @Override
344    protected void parseLong(CacheBuilderSpec spec, long value) {
345      checkArgument(spec.maximumWeight == null,
346          "maximum weight was already set to ", spec.maximumWeight);
347      checkArgument(spec.maximumSize == null,
348          "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(spec.concurrencyLevel == null,
358          "concurrency level was already set to ", 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, @Nullable 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, @Nullable String value) {
389      checkArgument(value == null, "key %s does not take values", key);
390      checkArgument(spec.valueStrength == null,
391        "%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, @Nullable 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(
411        CacheBuilderSpec spec,
412        long duration,
413        TimeUnit unit);
414
415    @Override
416    public void parse(CacheBuilderSpec spec, String key, String value) {
417      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
418      try {
419        char lastChar = value.charAt(value.length() - 1);
420        TimeUnit timeUnit;
421        switch (lastChar) {
422          case 'd':
423            timeUnit = TimeUnit.DAYS;
424            break;
425          case 'h':
426            timeUnit = TimeUnit.HOURS;
427            break;
428          case 'm':
429            timeUnit = TimeUnit.MINUTES;
430            break;
431          case 's':
432            timeUnit = TimeUnit.SECONDS;
433            break;
434          default:
435            throw new IllegalArgumentException(
436                String.format("key %s invalid format.  was %s, must end with one of [dDhHmMsS]",
437                    key, value));
438        }
439
440        long duration = Long.parseLong(value.substring(0, value.length() - 1));
441        parseDuration(spec, duration, timeUnit);
442      } catch (NumberFormatException e) {
443        throw new IllegalArgumentException(
444            String.format("key %s value set to %s, must be integer", key, value));
445      }
446    }
447  }
448
449  /** Parse expireAfterAccess */
450  static class AccessDurationParser extends DurationParser {
451    @Override 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 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
461      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
462      spec.writeExpirationDuration = duration;
463      spec.writeExpirationTimeUnit = unit;
464    }
465  }
466
467  /** Parse refreshAfterWrite */
468  static class RefreshDurationParser extends DurationParser {
469    @Override 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