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