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