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