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