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