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