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 .build(); 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 ", 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(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize); 342 checkArgument( 343 spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight); 344 spec.maximumSize = value; 345 } 346 } 347 348 /** Parse maximumWeight */ 349 static class MaximumWeightParser extends LongParser { 350 @Override 351 protected void parseLong(CacheBuilderSpec spec, long value) { 352 checkArgument( 353 spec.maximumWeight == null, "maximum weight was already set to ", spec.maximumWeight); 354 checkArgument(spec.maximumSize == null, "maximum size was already set to ", spec.maximumSize); 355 spec.maximumWeight = value; 356 } 357 } 358 359 /** Parse concurrencyLevel */ 360 static class ConcurrencyLevelParser extends IntegerParser { 361 @Override 362 protected void parseInteger(CacheBuilderSpec spec, int value) { 363 checkArgument( 364 spec.concurrencyLevel == null, 365 "concurrency level was already set to ", 366 spec.concurrencyLevel); 367 spec.concurrencyLevel = value; 368 } 369 } 370 371 /** Parse weakKeys */ 372 static class KeyStrengthParser implements ValueParser { 373 private final Strength strength; 374 375 public KeyStrengthParser(Strength strength) { 376 this.strength = strength; 377 } 378 379 @Override 380 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 381 checkArgument(value == null, "key %s does not take values", key); 382 checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength); 383 spec.keyStrength = strength; 384 } 385 } 386 387 /** Parse weakValues and softValues */ 388 static class ValueStrengthParser implements ValueParser { 389 private final Strength strength; 390 391 public ValueStrengthParser(Strength strength) { 392 this.strength = strength; 393 } 394 395 @Override 396 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 397 checkArgument(value == null, "key %s does not take values", key); 398 checkArgument( 399 spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength); 400 401 spec.valueStrength = strength; 402 } 403 } 404 405 /** Parse recordStats */ 406 static class RecordStatsParser implements ValueParser { 407 408 @Override 409 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 410 checkArgument(value == null, "recordStats does not take values"); 411 checkArgument(spec.recordStats == null, "recordStats already set"); 412 spec.recordStats = true; 413 } 414 } 415 416 /** Base class for parsing times with durations */ 417 abstract static class DurationParser implements ValueParser { 418 protected abstract void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit); 419 420 @Override 421 public void parse(CacheBuilderSpec spec, String key, @CheckForNull String value) { 422 if (isNullOrEmpty(value)) { 423 throw new IllegalArgumentException("value of key " + key + " omitted"); 424 } 425 try { 426 char lastChar = value.charAt(value.length() - 1); 427 TimeUnit timeUnit; 428 switch (lastChar) { 429 case 'd': 430 timeUnit = TimeUnit.DAYS; 431 break; 432 case 'h': 433 timeUnit = TimeUnit.HOURS; 434 break; 435 case 'm': 436 timeUnit = TimeUnit.MINUTES; 437 break; 438 case 's': 439 timeUnit = TimeUnit.SECONDS; 440 break; 441 default: 442 throw new IllegalArgumentException( 443 format("key %s invalid unit: was %s, must end with one of [dhms]", key, value)); 444 } 445 446 long duration = Long.parseLong(value.substring(0, value.length() - 1)); 447 parseDuration(spec, duration, timeUnit); 448 } catch (NumberFormatException e) { 449 throw new IllegalArgumentException( 450 format("key %s value set to %s, must be integer", key, value)); 451 } 452 } 453 } 454 455 /** Parse expireAfterAccess */ 456 static class AccessDurationParser extends DurationParser { 457 @Override 458 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 459 checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set"); 460 spec.accessExpirationDuration = duration; 461 spec.accessExpirationTimeUnit = unit; 462 } 463 } 464 465 /** Parse expireAfterWrite */ 466 static class WriteDurationParser extends DurationParser { 467 @Override 468 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 469 checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set"); 470 spec.writeExpirationDuration = duration; 471 spec.writeExpirationTimeUnit = unit; 472 } 473 } 474 475 /** Parse refreshAfterWrite */ 476 static class RefreshDurationParser extends DurationParser { 477 @Override 478 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 479 checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set"); 480 spec.refreshDuration = duration; 481 spec.refreshTimeUnit = unit; 482 } 483 } 484 485 private static String format(String format, Object... args) { 486 return String.format(Locale.ROOT, format, args); 487 } 488}