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