View Javadoc
1   /*
2    * Copyright (C) 2011 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  
15  package com.google.common.cache;
16  
17  import static com.google.common.base.Preconditions.checkArgument;
18  
19  import com.google.common.annotations.GwtIncompatible;
20  import com.google.common.annotations.VisibleForTesting;
21  import com.google.common.base.MoreObjects;
22  import com.google.common.base.Objects;
23  import com.google.common.base.Splitter;
24  import com.google.common.cache.LocalCache.Strength;
25  import com.google.common.collect.ImmutableList;
26  import com.google.common.collect.ImmutableMap;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.concurrent.TimeUnit;
30  import javax.annotation.Nullable;
31  
32  /**
33   * A specification of a {@link CacheBuilder} configuration.
34   *
35   * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which makes it
36   * especially useful for command-line configuration of a {@code CacheBuilder}.
37   *
38   * <p>The string syntax is a series of comma-separated keys or key-value pairs, each corresponding
39   * to a {@code CacheBuilder} method.
40   * <ul>
41   * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
42   * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
43   * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
44   * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
45   * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
46   * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
47   * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
48   * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
49   * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
50   * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
51   * <li>{@code recordStats}: sets {@link CacheBuilder#recordStats}.
52   * </ul>
53   *
54   * <p>The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys will
55   * never be removed.
56   *
57   * <p>Durations are represented by an integer, followed by one of "d", "h", "m", or "s",
58   * representing days, hours, minutes, or seconds respectively. (There is currently no syntax to
59   * request expiration in milliseconds, microseconds, or nanoseconds.)
60   *
61   * <p>Whitespace before and after commas and equal signs is ignored. Keys may not be repeated; it is
62   * also illegal to use the following pairs of keys in a single value:
63   * <ul>
64   * <li>{@code maximumSize} and {@code maximumWeight}
65   * <li>{@code softValues} and {@code weakValues}
66   * </ul>
67   *
68   * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods with
69   * non-value parameters. These must be configured in code.
70   *
71   * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
72   * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
73   *
74   * @author Adam Winer
75   * @since 12.0
76   */
77  @GwtIncompatible
78  public final class CacheBuilderSpec {
79    /** Parses a single value. */
80    private interface ValueParser {
81      void parse(CacheBuilderSpec spec, String key, @Nullable String value);
82    }
83  
84    /** Splits each key-value pair. */
85    private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
86  
87    /** Splits the key from the value. */
88    private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
89  
90    /** Map of names to ValueParser. */
91    private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
92        ImmutableMap.<String, ValueParser>builder()
93            .put("initialCapacity", new InitialCapacityParser())
94            .put("maximumSize", new MaximumSizeParser())
95            .put("maximumWeight", new MaximumWeightParser())
96            .put("concurrencyLevel", new ConcurrencyLevelParser())
97            .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
98            .put("softValues", new ValueStrengthParser(Strength.SOFT))
99            .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 }