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 087public final class CacheBuilderSpec { 088 /** Parses a single value. */ 089 private interface ValueParser { 090 void parse(CacheBuilderSpec spec, String key, @CheckForNull String value); 091 } 092 093 /** Splits each key-value pair. */ 094 private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults(); 095 096 /** Splits the key from the value. */ 097 private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults(); 098 099 /** Map of names to ValueParser. */ 100 private static final ImmutableMap<String, ValueParser> VALUE_PARSERS = 101 ImmutableMap.<String, ValueParser>builder() 102 .put("initialCapacity", new InitialCapacityParser()) 103 .put("maximumSize", new MaximumSizeParser()) 104 .put("maximumWeight", new MaximumWeightParser()) 105 .put("concurrencyLevel", new ConcurrencyLevelParser()) 106 .put("weakKeys", new KeyStrengthParser(Strength.WEAK)) 107 .put("softValues", new ValueStrengthParser(Strength.SOFT)) 108 .put("weakValues", new ValueStrengthParser(Strength.WEAK)) 109 .put("recordStats", new RecordStatsParser()) 110 .put("expireAfterAccess", new AccessDurationParser()) 111 .put("expireAfterWrite", new WriteDurationParser()) 112 .put("refreshAfterWrite", new RefreshDurationParser()) 113 .put("refreshInterval", new RefreshDurationParser()) 114 .buildOrThrow(); 115 116 @VisibleForTesting @CheckForNull Integer initialCapacity; 117 @VisibleForTesting @CheckForNull Long maximumSize; 118 @VisibleForTesting @CheckForNull Long maximumWeight; 119 @VisibleForTesting @CheckForNull Integer concurrencyLevel; 120 @VisibleForTesting @CheckForNull Strength keyStrength; 121 @VisibleForTesting @CheckForNull Strength valueStrength; 122 @VisibleForTesting @CheckForNull Boolean recordStats; 123 @VisibleForTesting long writeExpirationDuration; 124 @VisibleForTesting @CheckForNull TimeUnit writeExpirationTimeUnit; 125 @VisibleForTesting long accessExpirationDuration; 126 @VisibleForTesting @CheckForNull TimeUnit accessExpirationTimeUnit; 127 @VisibleForTesting long refreshDuration; 128 @VisibleForTesting @CheckForNull TimeUnit refreshTimeUnit; 129 /** Specification; used for toParseableString(). */ 130 private final String specification; 131 132 private CacheBuilderSpec(String specification) { 133 this.specification = specification; 134 } 135 136 /** 137 * Creates a CacheBuilderSpec from a string. 138 * 139 * @param cacheBuilderSpecification the string form 140 */ 141 public static CacheBuilderSpec parse(String cacheBuilderSpecification) { 142 CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification); 143 if (!cacheBuilderSpecification.isEmpty()) { 144 for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) { 145 List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair)); 146 checkArgument(!keyAndValue.isEmpty(), "blank key-value pair"); 147 checkArgument( 148 keyAndValue.size() <= 2, 149 "key-value pair %s with more than one equals sign", 150 keyValuePair); 151 152 // Find the ValueParser for the current key. 153 String key = keyAndValue.get(0); 154 ValueParser valueParser = VALUE_PARSERS.get(key); 155 checkArgument(valueParser != null, "unknown key %s", key); 156 157 String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1); 158 valueParser.parse(spec, key, value); 159 } 160 } 161 162 return spec; 163 } 164 165 /** Returns a CacheBuilderSpec that will prevent caching. */ 166 public static CacheBuilderSpec disableCaching() { 167 // Maximum size of zero is one way to block caching 168 return CacheBuilderSpec.parse("maximumSize=0"); 169 } 170 171 /** Returns a CacheBuilder configured according to this instance's specification. */ 172 CacheBuilder<Object, Object> toCacheBuilder() { 173 CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder(); 174 if (initialCapacity != null) { 175 builder.initialCapacity(initialCapacity); 176 } 177 if (maximumSize != null) { 178 builder.maximumSize(maximumSize); 179 } 180 if (maximumWeight != null) { 181 builder.maximumWeight(maximumWeight); 182 } 183 if (concurrencyLevel != null) { 184 builder.concurrencyLevel(concurrencyLevel); 185 } 186 if (keyStrength != null) { 187 switch (keyStrength) { 188 case WEAK: 189 builder.weakKeys(); 190 break; 191 default: 192 throw new AssertionError(); 193 } 194 } 195 if (valueStrength != null) { 196 switch (valueStrength) { 197 case SOFT: 198 builder.softValues(); 199 break; 200 case WEAK: 201 builder.weakValues(); 202 break; 203 default: 204 throw new AssertionError(); 205 } 206 } 207 if (recordStats != null && recordStats) { 208 builder.recordStats(); 209 } 210 if (writeExpirationTimeUnit != null) { 211 builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit); 212 } 213 if (accessExpirationTimeUnit != null) { 214 builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit); 215 } 216 if (refreshTimeUnit != null) { 217 builder.refreshAfterWrite(refreshDuration, refreshTimeUnit); 218 } 219 220 return builder; 221 } 222 223 /** 224 * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order 225 * and form of this representation is not guaranteed, except that reparsing its output will 226 * produce a {@code CacheBuilderSpec} equal to this instance. 227 */ 228 public String toParsableString() { 229 return specification; 230 } 231 232 /** 233 * Returns a string representation for this CacheBuilderSpec instance. The form of this 234 * representation is not guaranteed. 235 */ 236 @Override 237 public String toString() { 238 return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString(); 239 } 240 241 @Override 242 public int hashCode() { 243 return Objects.hashCode( 244 initialCapacity, 245 maximumSize, 246 maximumWeight, 247 concurrencyLevel, 248 keyStrength, 249 valueStrength, 250 recordStats, 251 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 252 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 253 durationInNanos(refreshDuration, refreshTimeUnit)); 254 } 255 256 @Override 257 public boolean equals(@CheckForNull Object obj) { 258 if (this == obj) { 259 return true; 260 } 261 if (!(obj instanceof CacheBuilderSpec)) { 262 return false; 263 } 264 CacheBuilderSpec that = (CacheBuilderSpec) obj; 265 return Objects.equal(initialCapacity, that.initialCapacity) 266 && Objects.equal(maximumSize, that.maximumSize) 267 && Objects.equal(maximumWeight, that.maximumWeight) 268 && Objects.equal(concurrencyLevel, that.concurrencyLevel) 269 && Objects.equal(keyStrength, that.keyStrength) 270 && Objects.equal(valueStrength, that.valueStrength) 271 && Objects.equal(recordStats, that.recordStats) 272 && Objects.equal( 273 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 274 durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit)) 275 && Objects.equal( 276 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 277 durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit)) 278 && Objects.equal( 279 durationInNanos(refreshDuration, refreshTimeUnit), 280 durationInNanos(that.refreshDuration, that.refreshTimeUnit)); 281 } 282 283 /** 284 * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses 285 * nanos to match CacheBuilder implementation. 286 */ 287 @CheckForNull 288 private static Long durationInNanos(long duration, @CheckForNull TimeUnit unit) { 289 return (unit == null) ? null : unit.toNanos(duration); 290 } 291 292 /** Base class for parsing integers. */ 293 abstract static class IntegerParser implements ValueParser { 294 protected abstract void parseInteger(CacheBuilderSpec spec, int value); 295 296 @Override 297 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 298 if (isNullOrEmpty(value)) { 299 throw new IllegalArgumentException("value of key " + key + " omitted"); 300 } 301 try { 302 parseInteger(spec, Integer.parseInt(value)); 303 } catch (NumberFormatException e) { 304 throw new IllegalArgumentException( 305 format("key %s value set to %s, must be integer", key, value), e); 306 } 307 } 308 } 309 310 /** Base class for parsing integers. */ 311 abstract static class LongParser implements ValueParser { 312 protected abstract void parseLong(CacheBuilderSpec spec, long value); 313 314 @Override 315 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 316 if (isNullOrEmpty(value)) { 317 throw new IllegalArgumentException("value of key " + key + " omitted"); 318 } 319 try { 320 parseLong(spec, Long.parseLong(value)); 321 } catch (NumberFormatException e) { 322 throw new IllegalArgumentException( 323 format("key %s value set to %s, must be integer", key, value), e); 324 } 325 } 326 } 327 328 /** Parse initialCapacity */ 329 static class InitialCapacityParser extends IntegerParser { 330 @Override 331 protected void parseInteger(CacheBuilderSpec spec, int value) { 332 checkArgument( 333 spec.initialCapacity == null, 334 "initial capacity was already set to %s", 335 spec.initialCapacity); 336 spec.initialCapacity = value; 337 } 338 } 339 340 /** Parse maximumSize */ 341 static class MaximumSizeParser extends LongParser { 342 @Override 343 protected void parseLong(CacheBuilderSpec spec, long value) { 344 checkArgument( 345 spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize); 346 checkArgument( 347 spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight); 348 spec.maximumSize = value; 349 } 350 } 351 352 /** Parse maximumWeight */ 353 static class MaximumWeightParser extends LongParser { 354 @Override 355 protected void parseLong(CacheBuilderSpec spec, long value) { 356 checkArgument( 357 spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight); 358 checkArgument( 359 spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize); 360 spec.maximumWeight = value; 361 } 362 } 363 364 /** Parse concurrencyLevel */ 365 static class ConcurrencyLevelParser extends IntegerParser { 366 @Override 367 protected void parseInteger(CacheBuilderSpec spec, int value) { 368 checkArgument( 369 spec.concurrencyLevel == null, 370 "concurrency level was already set to %s", 371 spec.concurrencyLevel); 372 spec.concurrencyLevel = value; 373 } 374 } 375 376 /** Parse weakKeys */ 377 static class KeyStrengthParser implements ValueParser { 378 private final Strength strength; 379 380 public KeyStrengthParser(Strength strength) { 381 this.strength = strength; 382 } 383 384 @Override 385 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 386 checkArgument(value == null, "key %s does not take values", key); 387 checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength); 388 spec.keyStrength = strength; 389 } 390 } 391 392 /** Parse weakValues and softValues */ 393 static class ValueStrengthParser implements ValueParser { 394 private final Strength strength; 395 396 public ValueStrengthParser(Strength strength) { 397 this.strength = strength; 398 } 399 400 @Override 401 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 402 checkArgument(value == null, "key %s does not take values", key); 403 checkArgument( 404 spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength); 405 406 spec.valueStrength = strength; 407 } 408 } 409 410 /** Parse recordStats */ 411 static class RecordStatsParser implements ValueParser { 412 413 @Override 414 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 415 checkArgument(value == null, "recordStats does not take values"); 416 checkArgument(spec.recordStats == null, "recordStats already set"); 417 spec.recordStats = true; 418 } 419 } 420 421 /** Base class for parsing times with durations */ 422 abstract static class DurationParser implements ValueParser { 423 protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit); 424 425 @Override 426 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 427 if (isNullOrEmpty(value)) { 428 throw new IllegalArgumentException("value of key " + key + " omitted"); 429 } 430 try { 431 char lastChar = value.charAt(value.length() - 1); 432 TimeUnit timeUnit; 433 switch (lastChar) { 434 case 'd': 435 timeUnit = DAYS; 436 break; 437 case 'h': 438 timeUnit = HOURS; 439 break; 440 case 'm': 441 timeUnit = MINUTES; 442 break; 443 case 's': 444 timeUnit = SECONDS; 445 break; 446 default: 447 throw new IllegalArgumentException( 448 format("key %s invalid unit: was %s, must end with one of [dhms]", key, value)); 449 } 450 451 long duration = Long.parseLong(value.substring(0, value.length() - 1)); 452 parseDuration(spec, duration, timeUnit); 453 } catch (NumberFormatException e) { 454 throw new IllegalArgumentException( 455 format("key %s value set to %s, must be integer", key, value)); 456 } 457 } 458 } 459 460 /** Parse expireAfterAccess */ 461 static class AccessDurationParser extends DurationParser { 462 @Override 463 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 464 checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set"); 465 spec.accessExpirationDuration = duration; 466 spec.accessExpirationTimeUnit = unit; 467 } 468 } 469 470 /** Parse expireAfterWrite */ 471 static class WriteDurationParser extends DurationParser { 472 @Override 473 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 474 checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set"); 475 spec.writeExpirationDuration = duration; 476 spec.writeExpirationTimeUnit = unit; 477 } 478 } 479 480 /** Parse refreshAfterWrite */ 481 static class RefreshDurationParser extends DurationParser { 482 @Override 483 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 484 checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set"); 485 spec.refreshDuration = duration; 486 spec.refreshTimeUnit = unit; 487 } 488 } 489 490 private static String format(String format, Object... args) { 491 return String.format(Locale.ROOT, format, args); 492 } 493}