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