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