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 
17 package com.google.common.cache;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 
21 import com.google.common.annotations.Beta;
22 import com.google.common.annotations.VisibleForTesting;
23 import com.google.common.base.Objects;
24 import com.google.common.base.Splitter;
25 import com.google.common.cache.LocalCache.Strength;
26 import com.google.common.collect.ImmutableList;
27 import com.google.common.collect.ImmutableMap;
28 
29 import java.util.List;
30 import java.util.concurrent.TimeUnit;
31 
32 import javax.annotation.Nullable;
33 
34 /**
35  * A specification of a {@link CacheBuilder} configuration.
36  *
37  * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
38  * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
39  *
40  * <p>The string syntax is a series of comma-separated keys or key-value pairs,
41  * each corresponding to a {@code CacheBuilder} method.
42  * <ul>
43  * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
44  * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
45  * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
46  * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
47  * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
48  * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
49  * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
50  * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
51  * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
52  * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
53  * <li>{@code recordStats}: sets {@link CacheBuilder#recordStats}.
54  * </ul>
55  *
56  * <p>The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys
57  * will never be removed.
58  *
59  * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
60  * or "s", representing days, hours, minutes, or seconds respectively.  (There
61  * is currently no syntax to request expiration in milliseconds, microseconds,
62  * or nanoseconds.)
63  *
64  * <p>Whitespace before and after commas and equal signs is ignored.  Keys may
65  * not be repeated;  it is also illegal to use the following pairs of keys in
66  * a single value:
67  * <ul>
68  * <li>{@code maximumSize} and {@code maximumWeight}
69  * <li>{@code softValues} and {@code weakValues}
70  * </ul>
71  *
72  * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
73  * with non-value parameters.  These must be configured in code.
74  *
75  * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
76  * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
77  *
78  * @author Adam Winer
79  * @since 12.0
80  */
81 @Beta
82 public final class CacheBuilderSpec {
83   /** Parses a single value. */
84   private interface ValueParser {
parse(CacheBuilderSpec spec, String key, @Nullable String value)85     void parse(CacheBuilderSpec spec, String key, @Nullable String value);
86   }
87 
88   /** Splits each key-value pair. */
89   private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
90 
91   /** Splits the key from the value. */
92   private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
93 
94   /** Map of names to ValueParser. */
95   private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
96       ImmutableMap.<String, ValueParser>builder()
97           .put("initialCapacity", new InitialCapacityParser())
98           .put("maximumSize", new MaximumSizeParser())
99           .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 
CacheBuilderSpec(String specification)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    */
parse(String cacheBuilderSpecification)136   public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
137     CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
138     if (cacheBuilderSpecification.length() != 0) {
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    */
disableCaching()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    */
toCacheBuilder()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    */
toParsableString()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
toString()235   public String toString() {
236     return Objects.toStringHelper(this).addValue(toParsableString()).toString();
237   }
238 
239   @Override
hashCode()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
equals(@ullable Object obj)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    */
durationInNanos(long duration, @Nullable TimeUnit unit)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 {
parseInteger(CacheBuilderSpec spec, int value)288     protected abstract void parseInteger(CacheBuilderSpec spec, int value);
289 
290     @Override
parse(CacheBuilderSpec spec, String key, String value)291     public void parse(CacheBuilderSpec spec, String key, String value) {
292       checkArgument(value != null && value.length() != 0, "value of key %s omitted", key);
293       try {
294         parseInteger(spec, Integer.parseInt(value));
295       } catch (NumberFormatException e) {
296         throw new IllegalArgumentException(
297             String.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 {
parseLong(CacheBuilderSpec spec, long value)304     protected abstract void parseLong(CacheBuilderSpec spec, long value);
305 
306     @Override
parse(CacheBuilderSpec spec, String key, String value)307     public void parse(CacheBuilderSpec spec, String key, String value) {
308       checkArgument(value != null && value.length() != 0, "value of key %s omitted", key);
309       try {
310         parseLong(spec, Long.parseLong(value));
311       } catch (NumberFormatException e) {
312         throw new IllegalArgumentException(
313             String.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
parseInteger(CacheBuilderSpec spec, int value)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
parseLong(CacheBuilderSpec spec, long value)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
parseLong(CacheBuilderSpec spec, long value)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
parseInteger(CacheBuilderSpec spec, int value)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 
KeyStrengthParser(Strength strength)366     public KeyStrengthParser(Strength strength) {
367       this.strength = strength;
368     }
369 
370     @Override
parse(CacheBuilderSpec spec, String key, @Nullable String value)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 
ValueStrengthParser(Strength strength)382     public ValueStrengthParser(Strength strength) {
383       this.strength = strength;
384     }
385 
386     @Override
parse(CacheBuilderSpec spec, String key, @Nullable String value)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
parse(CacheBuilderSpec spec, String key, @Nullable String value)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 {
parseDuration( CacheBuilderSpec spec, long duration, TimeUnit unit)409     protected abstract void parseDuration(
410         CacheBuilderSpec spec,
411         long duration,
412         TimeUnit unit);
413 
414     @Override
parse(CacheBuilderSpec spec, String key, String value)415     public void parse(CacheBuilderSpec spec, String key, String value) {
416       checkArgument(value != null && value.length() != 0, "value of key %s omitted", key);
417       try {
418         char lastChar = value.charAt(value.length() - 1);
419         long multiplier = 1;
420         switch (lastChar) {
421           case 'd':
422             multiplier *= 24;
423           case 'h':
424             multiplier *= 60;
425           case 'm':
426             multiplier *= 60;
427           case 's':
428             break;
429           default:
430             throw new IllegalArgumentException(
431                 String.format("key %s invalid format.  was %s, must end with one of [dDhHmMsS]",
432                     key, value));
433         }
434 
435         long duration = Long.parseLong(value.substring(0, value.length() - 1));
436         parseDuration(spec, duration * multiplier, TimeUnit.SECONDS);
437       } catch (NumberFormatException e) {
438         throw new IllegalArgumentException(
439             String.format("key %s value set to %s, must be integer", key, value));
440       }
441     }
442   }
443 
444   /** Parse expireAfterAccess */
445   static class AccessDurationParser extends DurationParser {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)446     @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
447       checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
448       spec.accessExpirationDuration = duration;
449       spec.accessExpirationTimeUnit = unit;
450     }
451   }
452 
453   /** Parse expireAfterWrite */
454   static class WriteDurationParser extends DurationParser {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)455     @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
456       checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
457       spec.writeExpirationDuration = duration;
458       spec.writeExpirationTimeUnit = unit;
459     }
460   }
461 
462   /** Parse refreshAfterWrite */
463   static class RefreshDurationParser extends DurationParser {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)464     @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
465       checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
466       spec.refreshDuration = duration;
467       spec.refreshTimeUnit = unit;
468     }
469   }
470 }
471