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
017 package com.google.common.cache;
018
019 import static com.google.common.base.Preconditions.checkArgument;
020
021 import com.google.common.annotations.Beta;
022 import com.google.common.annotations.VisibleForTesting;
023 import com.google.common.base.Objects;
024 import com.google.common.base.Splitter;
025 import com.google.common.cache.LocalCache.Strength;
026 import com.google.common.collect.ImmutableList;
027 import com.google.common.collect.ImmutableMap;
028
029 import java.util.List;
030 import java.util.concurrent.TimeUnit;
031
032 import javax.annotation.Nullable;
033
034 /**
035 * A specification of a {@link CacheBuilder} configuration.
036 *
037 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
038 * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
039 *
040 * <p>The string syntax is a series of comma-separated keys or key-value pairs,
041 * each corresponding to a {@code CacheBuilder} method.
042 * <ul>
043 * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
044 * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
045 * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
046 * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
047 * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
048 * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
049 * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
050 * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
051 * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
052 * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
053 * </ul>
054 *
055 * The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys
056 * will never be removed.
057 *
058 * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
059 * or "s", representing days, hours, minutes, or seconds respectively. (There
060 * is currently no syntax to request expiration in milliseconds, microseconds,
061 * or nanoseconds.)
062 *
063 * <p>Whitespace before and after commas and equal signs is ignored. Keys may
064 * not be repeated; it is also illegal to use the following pairs of keys in
065 * a single value:
066 * <ul>
067 * <li>{@code maximumSize} and {@code maximumWeight}
068 * <li>{@code softValues} and {@code weakValues}
069 * </ul>
070 *
071 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
072 * with non-value parameters. These must be configured in code.
073 *
074 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
075 * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
076 *
077 * @author Adam Winer
078 * @since 12.0
079 */
080 @Beta
081 public final class CacheBuilderSpec {
082 /** Parses a single value. */
083 private interface ValueParser {
084 void parse(CacheBuilderSpec spec, String key, @Nullable String value);
085 }
086
087 /** Splits each key-value pair. */
088 private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
089
090 /** Splits the key from the value. */
091 private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
092
093 /** Map of names to ValueParser. */
094 private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
095 ImmutableMap.<String, ValueParser>builder()
096 .put("initialCapacity", new InitialCapacityParser())
097 .put("maximumSize", new MaximumSizeParser())
098 .put("maximumWeight", new MaximumWeightParser())
099 .put("concurrencyLevel", new ConcurrencyLevelParser())
100 .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
101 .put("softValues", new ValueStrengthParser(Strength.SOFT))
102 .put("weakValues", new ValueStrengthParser(Strength.WEAK))
103 .put("expireAfterAccess", new AccessDurationParser())
104 .put("expireAfterWrite", new WriteDurationParser())
105 .put("refreshAfterWrite", new RefreshDurationParser())
106 .put("refreshInterval", new RefreshDurationParser())
107 .build();
108
109 @VisibleForTesting Integer initialCapacity;
110 @VisibleForTesting Long maximumSize;
111 @VisibleForTesting Long maximumWeight;
112 @VisibleForTesting Integer concurrencyLevel;
113 @VisibleForTesting Strength keyStrength;
114 @VisibleForTesting Strength valueStrength;
115 @VisibleForTesting long writeExpirationDuration;
116 @VisibleForTesting TimeUnit writeExpirationTimeUnit;
117 @VisibleForTesting long accessExpirationDuration;
118 @VisibleForTesting TimeUnit accessExpirationTimeUnit;
119 @VisibleForTesting long refreshDuration;
120 @VisibleForTesting TimeUnit refreshTimeUnit;
121 /** Specification; used for toParseableString(). */
122 private final String specification;
123
124 private CacheBuilderSpec(String specification) {
125 this.specification = specification;
126 }
127
128 /**
129 * Creates a CacheBuilderSpec from a string.
130 *
131 * @param cacheBuilderSpecification the string form
132 */
133 public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
134 CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
135 if (cacheBuilderSpecification.length() > 0) {
136 for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
137 List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
138 checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
139 checkArgument(keyAndValue.size() <= 2,
140 "key-value pair %s with more than one equals sign", keyValuePair);
141
142 // Find the ValueParser for the current key.
143 String key = keyAndValue.get(0);
144 ValueParser valueParser = VALUE_PARSERS.get(key);
145 checkArgument(valueParser != null, "unknown key %s", key);
146
147 String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
148 valueParser.parse(spec, key, value);
149 }
150 }
151
152 return spec;
153 }
154
155 /**
156 * Returns a CacheBuilderSpec that will prevent caching.
157 */
158 public static CacheBuilderSpec disableCaching() {
159 // Maximum size of zero is one way to block caching
160 return CacheBuilderSpec.parse("maximumSize=0");
161 }
162
163 /**
164 * Returns a CacheBuilder configured according to this instance's specification.
165 */
166 CacheBuilder<Object, Object> toCacheBuilder() {
167 CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
168 if (initialCapacity != null) {
169 builder.initialCapacity(initialCapacity);
170 }
171 if (maximumSize != null) {
172 builder.maximumSize(maximumSize);
173 }
174 if (maximumWeight != null) {
175 builder.maximumWeight(maximumWeight);
176 }
177 if (concurrencyLevel != null) {
178 builder.concurrencyLevel(concurrencyLevel);
179 }
180 if (keyStrength != null) {
181 switch (keyStrength) {
182 case WEAK:
183 builder.weakKeys();
184 break;
185 default:
186 throw new AssertionError();
187 }
188 }
189 if (valueStrength != null) {
190 switch (valueStrength) {
191 case SOFT:
192 builder.softValues();
193 break;
194 case WEAK:
195 builder.weakValues();
196 break;
197 default:
198 throw new AssertionError();
199 }
200 }
201 if (writeExpirationTimeUnit != null) {
202 builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
203 }
204 if (accessExpirationTimeUnit != null) {
205 builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
206 }
207 if (refreshTimeUnit != null) {
208 builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
209 }
210
211 return builder;
212 }
213
214 /**
215 * Returns a string that can be used to parse an equivalent
216 * {@code CacheBuilderSpec}. The order and form of this representation is
217 * not guaranteed, except that reparsing its output will produce
218 * a {@code CacheBuilderSpec} equal to this instance.
219 */
220 public String toParsableString() {
221 return specification;
222 }
223
224 /**
225 * Returns a string representation for this CacheBuilderSpec instance.
226 * The form of this representation is not guaranteed.
227 */
228
229 @Override
230 public String toString() {
231 return Objects.toStringHelper(this).addValue(toParsableString()).toString();
232 }
233
234
235 @Override
236 public int hashCode() {
237 return Objects.hashCode(
238 initialCapacity,
239 maximumSize,
240 maximumWeight,
241 concurrencyLevel,
242 keyStrength,
243 valueStrength,
244 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
245 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
246 durationInNanos(refreshDuration, refreshTimeUnit));
247 }
248
249
250 @Override
251 public boolean equals(Object obj) {
252 if (this == obj) {
253 return true;
254 }
255 if (!(obj instanceof CacheBuilderSpec)) {
256 return false;
257 }
258 CacheBuilderSpec that = (CacheBuilderSpec) obj;
259 return Objects.equal(initialCapacity, that.initialCapacity)
260 && Objects.equal(maximumSize, that.maximumSize)
261 && Objects.equal(maximumWeight, that.maximumWeight)
262 && Objects.equal(concurrencyLevel, that.concurrencyLevel)
263 && Objects.equal(keyStrength, that.keyStrength)
264 && Objects.equal(valueStrength, that.valueStrength)
265 && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
266 durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
267 && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
268 durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
269 && Objects.equal(durationInNanos(refreshDuration, refreshTimeUnit),
270 durationInNanos(that.refreshDuration, that.refreshTimeUnit));
271 }
272
273 /**
274 * Converts an expiration duration/unit pair into a single Long for hashing and equality.
275 * Uses nanos to match CacheBuilder implementation.
276 */
277 @Nullable private static Long durationInNanos(long duration, @Nullable TimeUnit unit) {
278 return (unit == null) ? null : unit.toNanos(duration);
279 }
280
281 /** Base class for parsing integers. */
282 abstract static class IntegerParser implements ValueParser {
283 protected abstract void parseInteger(CacheBuilderSpec spec, int value);
284
285 public void parse(CacheBuilderSpec spec, String key, String value) {
286 checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
287 try {
288 parseInteger(spec, Integer.parseInt(value));
289 } catch (NumberFormatException e) {
290 throw new IllegalArgumentException(
291 String.format("key %s value set to %s, must be integer", key, value), e);
292 }
293 }
294 }
295
296 /** Base class for parsing integers. */
297 abstract static class LongParser implements ValueParser {
298 protected abstract void parseLong(CacheBuilderSpec spec, long value);
299
300 public void parse(CacheBuilderSpec spec, String key, String value) {
301 checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
302 try {
303 parseLong(spec, Long.parseLong(value));
304 } catch (NumberFormatException e) {
305 throw new IllegalArgumentException(
306 String.format("key %s value set to %s, must be integer", key, value), e);
307 }
308 }
309 }
310
311 /** Parse initialCapacity */
312 static class InitialCapacityParser extends IntegerParser {
313
314 @Override
315 protected void parseInteger(CacheBuilderSpec spec, int value) {
316 checkArgument(spec.initialCapacity == null,
317 "initial capacity was already set to ", spec.initialCapacity);
318 spec.initialCapacity = value;
319 }
320 }
321
322 /** Parse maximumSize */
323 static class MaximumSizeParser extends LongParser {
324
325 @Override
326 protected void parseLong(CacheBuilderSpec spec, long value) {
327 checkArgument(spec.maximumSize == null,
328 "maximum size was already set to ", spec.maximumSize);
329 checkArgument(spec.maximumWeight == null,
330 "maximum weight was already set to ", spec.maximumWeight);
331 spec.maximumSize = value;
332 }
333 }
334
335 /** Parse maximumWeight */
336 static class MaximumWeightParser extends LongParser {
337
338 @Override
339 protected void parseLong(CacheBuilderSpec spec, long value) {
340 checkArgument(spec.maximumWeight == null,
341 "maximum weight was already set to ", spec.maximumWeight);
342 checkArgument(spec.maximumSize == null,
343 "maximum size was already set to ", spec.maximumSize);
344 spec.maximumWeight = value;
345 }
346 }
347
348 /** Parse concurrencyLevel */
349 static class ConcurrencyLevelParser extends IntegerParser {
350
351 @Override
352 protected void parseInteger(CacheBuilderSpec spec, int value) {
353 checkArgument(spec.concurrencyLevel == null,
354 "concurrency level was already set to ", spec.concurrencyLevel);
355 spec.concurrencyLevel = value;
356 }
357 }
358
359 /** Parse weakKeys */
360 static class KeyStrengthParser implements ValueParser {
361 private final Strength strength;
362
363 public KeyStrengthParser(Strength strength) {
364 this.strength = strength;
365 }
366
367 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
368 checkArgument(value == null, "key %s does not take values", key);
369 checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
370 spec.keyStrength = strength;
371 }
372 }
373
374 /** Parse weakValues and softValues */
375 static class ValueStrengthParser implements ValueParser {
376 private final Strength strength;
377
378 public ValueStrengthParser(Strength strength) {
379 this.strength = strength;
380 }
381
382 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
383 checkArgument(value == null, "key %s does not take values", key);
384 checkArgument(spec.valueStrength == null,
385 "%s was already set to %s", key, spec.valueStrength);
386
387 spec.valueStrength = strength;
388 }
389 }
390
391 /** Base class for parsing times with durations */
392 abstract static class DurationParser implements ValueParser {
393 protected abstract void parseDuration(
394 CacheBuilderSpec spec,
395 long duration,
396 TimeUnit unit);
397
398 public void parse(CacheBuilderSpec spec, String key, String value) {
399 checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
400 try {
401 char lastChar = value.charAt(value.length() - 1);
402 TimeUnit timeUnit;
403 long multiplier = 1;
404 switch (lastChar) {
405 case 'd':
406 multiplier *= 24;
407 case 'h':
408 multiplier *= 60;
409 case 'm':
410 multiplier *= 60;
411 case 's':
412 timeUnit = TimeUnit.SECONDS;
413 break;
414 default:
415 throw new IllegalArgumentException(
416 String.format("key %s invalid format. was %s, must end with one of [dDhHmMsS]",
417 key, value));
418 }
419
420 long duration = multiplier * Long.parseLong(value.substring(0, value.length() - 1));
421 parseDuration(spec, duration, timeUnit);
422 } catch (NumberFormatException e) {
423 throw new IllegalArgumentException(
424 String.format("key %s value set to %s, must be integer", key, value));
425 }
426 }
427 }
428
429 /** Parse expireAfterAccess */
430 static class AccessDurationParser extends DurationParser {
431
432 @Override
433 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
434 checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
435 spec.accessExpirationDuration = duration;
436 spec.accessExpirationTimeUnit = unit;
437 }
438 }
439
440 /** Parse expireAfterWrite */
441 static class WriteDurationParser extends DurationParser {
442
443 @Override
444 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
445 checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
446 spec.writeExpirationDuration = duration;
447 spec.writeExpirationTimeUnit = unit;
448 }
449 }
450
451 /** Parse refreshAfterWrite */
452 static class RefreshDurationParser extends DurationParser {
453
454 @Override
455 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
456 checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
457 spec.refreshDuration = duration;
458 spec.refreshTimeUnit = unit;
459 }
460 }
461 }