001/* 002 * Copyright (C) 2011 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.cache; 018 019import static com.google.common.base.Preconditions.checkArgument; 020 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; 028 029import java.util.List; 030import java.util.Locale; 031import java.util.concurrent.TimeUnit; 032 033import javax.annotation.Nullable; 034 035/** 036 * A specification of a {@link CacheBuilder} configuration. 037 * 038 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which 039 * makes it especially useful for command-line configuration of a {@code CacheBuilder}. 040 * 041 * <p>The string syntax is a series of comma-separated keys or key-value pairs, 042 * each corresponding to a {@code CacheBuilder} method. 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 058 * will never be removed. 059 * 060 * <p>Durations are represented by an integer, followed by one of "d", "h", "m", 061 * or "s", representing days, hours, minutes, or seconds respectively. (There 062 * is currently no syntax to request expiration in milliseconds, microseconds, 063 * or nanoseconds.) 064 * 065 * <p>Whitespace before and after commas and equal signs is ignored. Keys may 066 * not be repeated; it is also illegal to use the following pairs of keys in 067 * a single value: 068 * <ul> 069 * <li>{@code maximumSize} and {@code maximumWeight} 070 * <li>{@code softValues} and {@code weakValues} 071 * </ul> 072 * 073 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods 074 * with non-value parameters. These must be configured in code. 075 * 076 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using 077 * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}. 078 * 079 * @author Adam Winer 080 * @since 12.0 081 */ 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 @VisibleForTesting Integer initialCapacity; 112 @VisibleForTesting Long maximumSize; 113 @VisibleForTesting Long maximumWeight; 114 @VisibleForTesting Integer concurrencyLevel; 115 @VisibleForTesting Strength keyStrength; 116 @VisibleForTesting Strength valueStrength; 117 @VisibleForTesting Boolean recordStats; 118 @VisibleForTesting long writeExpirationDuration; 119 @VisibleForTesting TimeUnit writeExpirationTimeUnit; 120 @VisibleForTesting long accessExpirationDuration; 121 @VisibleForTesting TimeUnit accessExpirationTimeUnit; 122 @VisibleForTesting long refreshDuration; 123 @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(keyAndValue.size() <= 2, 143 "key-value pair %s with more than one equals sign", keyValuePair); 144 145 // Find the ValueParser for the current key. 146 String key = keyAndValue.get(0); 147 ValueParser valueParser = VALUE_PARSERS.get(key); 148 checkArgument(valueParser != null, "unknown key %s", key); 149 150 String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1); 151 valueParser.parse(spec, key, value); 152 } 153 } 154 155 return spec; 156 } 157 158 /** 159 * Returns a CacheBuilderSpec that will prevent caching. 160 */ 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 /** 167 * Returns a CacheBuilder configured according to this instance's specification. 168 */ 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 222 * {@code CacheBuilderSpec}. The order and form of this representation is 223 * not guaranteed, except that reparsing its output will produce 224 * a {@code CacheBuilderSpec} equal to this instance. 225 */ 226 public String toParsableString() { 227 return specification; 228 } 229 230 /** 231 * Returns a string representation for this CacheBuilderSpec instance. 232 * The form of this representation is not guaranteed. 233 */ 234 @Override 235 public String toString() { 236 return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString(); 237 } 238 239 @Override 240 public int hashCode() { 241 return Objects.hashCode( 242 initialCapacity, 243 maximumSize, 244 maximumWeight, 245 concurrencyLevel, 246 keyStrength, 247 valueStrength, 248 recordStats, 249 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 250 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 251 durationInNanos(refreshDuration, refreshTimeUnit)); 252 } 253 254 @Override 255 public boolean equals(@Nullable Object obj) { 256 if (this == obj) { 257 return true; 258 } 259 if (!(obj instanceof CacheBuilderSpec)) { 260 return false; 261 } 262 CacheBuilderSpec that = (CacheBuilderSpec) obj; 263 return Objects.equal(initialCapacity, that.initialCapacity) 264 && Objects.equal(maximumSize, that.maximumSize) 265 && Objects.equal(maximumWeight, that.maximumWeight) 266 && Objects.equal(concurrencyLevel, that.concurrencyLevel) 267 && Objects.equal(keyStrength, that.keyStrength) 268 && Objects.equal(valueStrength, that.valueStrength) 269 && Objects.equal(recordStats, that.recordStats) 270 && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 271 durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit)) 272 && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 273 durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit)) 274 && Objects.equal(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. 280 * Uses nanos to match CacheBuilder implementation. 281 */ 282 @Nullable private static 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(spec.initialCapacity == null, 323 "initial capacity was already set to ", spec.initialCapacity); 324 spec.initialCapacity = value; 325 } 326 } 327 328 /** Parse maximumSize */ 329 static class MaximumSizeParser extends LongParser { 330 @Override 331 protected void parseLong(CacheBuilderSpec spec, long value) { 332 checkArgument(spec.maximumSize == null, 333 "maximum size was already set to ", spec.maximumSize); 334 checkArgument(spec.maximumWeight == null, 335 "maximum weight was already set to ", spec.maximumWeight); 336 spec.maximumSize = value; 337 } 338 } 339 340 /** Parse maximumWeight */ 341 static class MaximumWeightParser extends LongParser { 342 @Override 343 protected void parseLong(CacheBuilderSpec spec, long value) { 344 checkArgument(spec.maximumWeight == null, 345 "maximum weight was already set to ", spec.maximumWeight); 346 checkArgument(spec.maximumSize == null, 347 "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(spec.concurrencyLevel == null, 357 "concurrency level was already set to ", spec.concurrencyLevel); 358 spec.concurrencyLevel = value; 359 } 360 } 361 362 /** Parse weakKeys */ 363 static class KeyStrengthParser implements ValueParser { 364 private final Strength strength; 365 366 public KeyStrengthParser(Strength strength) { 367 this.strength = strength; 368 } 369 370 @Override 371 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 372 checkArgument(value == null, "key %s does not take values", key); 373 checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength); 374 spec.keyStrength = strength; 375 } 376 } 377 378 /** Parse weakValues and softValues */ 379 static class ValueStrengthParser implements ValueParser { 380 private final Strength strength; 381 382 public ValueStrengthParser(Strength strength) { 383 this.strength = strength; 384 } 385 386 @Override 387 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 388 checkArgument(value == null, "key %s does not take values", key); 389 checkArgument(spec.valueStrength == null, 390 "%s was already set to %s", key, spec.valueStrength); 391 392 spec.valueStrength = strength; 393 } 394 } 395 396 /** Parse recordStats */ 397 static class RecordStatsParser implements ValueParser { 398 399 @Override 400 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 401 checkArgument(value == null, "recordStats does not take values"); 402 checkArgument(spec.recordStats == null, "recordStats already set"); 403 spec.recordStats = true; 404 } 405 } 406 407 /** Base class for parsing times with durations */ 408 abstract static class DurationParser implements ValueParser { 409 protected abstract void parseDuration( 410 CacheBuilderSpec spec, 411 long duration, 412 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("key %s invalid format. was %s, must end with one of [dDhHmMsS]", 436 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 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 protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 460 checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set"); 461 spec.writeExpirationDuration = duration; 462 spec.writeExpirationTimeUnit = unit; 463 } 464 } 465 466 /** Parse refreshAfterWrite */ 467 static class RefreshDurationParser extends DurationParser { 468 @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) { 469 checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set"); 470 spec.refreshDuration = duration; 471 spec.refreshTimeUnit = unit; 472 } 473 } 474 475 private static String format(String format, Object... args) { 476 return String.format(Locale.ROOT, format, args); 477 } 478}