• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.MoreObjects;
24 import com.google.common.base.Objects;
25 import com.google.common.base.Splitter;
26 import com.google.common.cache.LocalCache.Strength;
27 import com.google.common.collect.ImmutableList;
28 import com.google.common.collect.ImmutableMap;
29 
30 import java.util.List;
31 import java.util.concurrent.TimeUnit;
32 
33 import 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
83 public final class CacheBuilderSpec {
84   /** Parses a single value. */
85   private interface ValueParser {
parse(CacheBuilderSpec spec, String key, @Nullable String value)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 
CacheBuilderSpec(String specification)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    */
parse(String cacheBuilderSpecification)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    */
disableCaching()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    */
toCacheBuilder()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    */
toParsableString()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
toString()236   public String toString() {
237     return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString();
238   }
239 
240   @Override
hashCode()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
equals(@ullable Object obj)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    */
durationInNanos(long duration, @Nullable TimeUnit unit)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 {
parseInteger(CacheBuilderSpec spec, int value)289     protected abstract void parseInteger(CacheBuilderSpec spec, int value);
290 
291     @Override
parse(CacheBuilderSpec spec, String key, String value)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 {
parseLong(CacheBuilderSpec spec, long value)305     protected abstract void parseLong(CacheBuilderSpec spec, long value);
306 
307     @Override
parse(CacheBuilderSpec spec, String key, String value)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
parseInteger(CacheBuilderSpec spec, int value)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
parseLong(CacheBuilderSpec spec, long value)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
parseLong(CacheBuilderSpec spec, long value)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
parseInteger(CacheBuilderSpec spec, int value)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 
KeyStrengthParser(Strength strength)367     public KeyStrengthParser(Strength strength) {
368       this.strength = strength;
369     }
370 
371     @Override
parse(CacheBuilderSpec spec, String key, @Nullable String value)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 
ValueStrengthParser(Strength strength)383     public ValueStrengthParser(Strength strength) {
384       this.strength = strength;
385     }
386 
387     @Override
parse(CacheBuilderSpec spec, String key, @Nullable String value)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
parse(CacheBuilderSpec spec, String key, @Nullable String value)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 {
parseDuration( CacheBuilderSpec spec, long duration, TimeUnit unit)410     protected abstract void parseDuration(
411         CacheBuilderSpec spec,
412         long duration,
413         TimeUnit unit);
414 
415     @Override
parse(CacheBuilderSpec spec, String key, String value)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 {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)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 {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)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 {
parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit)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