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