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 default: 197 throw new AssertionError(); 198 } 199 } 200 if (valueStrength != null) { 201 switch (valueStrength) { 202 case SOFT: 203 builder.softValues(); 204 break; 205 case WEAK: 206 builder.weakValues(); 207 break; 208 default: 209 throw new AssertionError(); 210 } 211 } 212 if (recordStats != null && recordStats) { 213 builder.recordStats(); 214 } 215 if (writeExpirationTimeUnit != null) { 216 builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit); 217 } 218 if (accessExpirationTimeUnit != null) { 219 builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit); 220 } 221 if (refreshTimeUnit != null) { 222 builder.refreshAfterWrite(refreshDuration, refreshTimeUnit); 223 } 224 225 return builder; 226 } 227 228 /** 229 * Returns a string that can be used to parse an equivalent {@code CacheBuilderSpec}. The order 230 * and form of this representation is not guaranteed, except that reparsing its output will 231 * produce a {@code CacheBuilderSpec} equal to this instance. 232 */ 233 public String toParsableString() { 234 return specification; 235 } 236 237 /** 238 * Returns a string representation for this CacheBuilderSpec instance. The form of this 239 * representation is not guaranteed. 240 */ 241 @Override 242 public String toString() { 243 return MoreObjects.toStringHelper(this).addValue(toParsableString()).toString(); 244 } 245 246 @Override 247 public int hashCode() { 248 return Objects.hash( 249 initialCapacity, 250 maximumSize, 251 maximumWeight, 252 concurrencyLevel, 253 keyStrength, 254 valueStrength, 255 recordStats, 256 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 257 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 258 durationInNanos(refreshDuration, refreshTimeUnit)); 259 } 260 261 @Override 262 public boolean equals(@Nullable Object obj) { 263 if (this == obj) { 264 return true; 265 } 266 if (!(obj instanceof CacheBuilderSpec)) { 267 return false; 268 } 269 CacheBuilderSpec that = (CacheBuilderSpec) obj; 270 return Objects.equals(initialCapacity, that.initialCapacity) 271 && Objects.equals(maximumSize, that.maximumSize) 272 && Objects.equals(maximumWeight, that.maximumWeight) 273 && Objects.equals(concurrencyLevel, that.concurrencyLevel) 274 && Objects.equals(keyStrength, that.keyStrength) 275 && Objects.equals(valueStrength, that.valueStrength) 276 && Objects.equals(recordStats, that.recordStats) 277 && Objects.equals( 278 durationInNanos(writeExpirationDuration, writeExpirationTimeUnit), 279 durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit)) 280 && Objects.equals( 281 durationInNanos(accessExpirationDuration, accessExpirationTimeUnit), 282 durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit)) 283 && Objects.equals( 284 durationInNanos(refreshDuration, refreshTimeUnit), 285 durationInNanos(that.refreshDuration, that.refreshTimeUnit)); 286 } 287 288 /** 289 * Converts an expiration duration/unit pair into a single Long for hashing and equality. Uses 290 * nanos to match CacheBuilder implementation. 291 */ 292 private static @Nullable Long durationInNanos(long duration, @Nullable TimeUnit unit) { 293 return (unit == null) ? null : unit.toNanos(duration); 294 } 295 296 /** Base class for parsing integers. */ 297 private abstract static class IntegerParser implements ValueParser { 298 abstract void writeToSpec(CacheBuilderSpec spec, int value); 299 300 @Override 301 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 302 if (isNullOrEmpty(value)) { 303 throw new IllegalArgumentException("value of key " + key + " omitted"); 304 } 305 try { 306 writeToSpec(spec, parseInt(value)); 307 } catch (NumberFormatException e) { 308 throw new IllegalArgumentException( 309 format("key %s value set to %s, must be integer", key, value), e); 310 } 311 } 312 } 313 314 /** Base class for parsing integers. */ 315 private abstract static class LongParser implements ValueParser { 316 abstract void writeToSpec(CacheBuilderSpec spec, long value); 317 318 @Override 319 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 320 if (isNullOrEmpty(value)) { 321 throw new IllegalArgumentException("value of key " + key + " omitted"); 322 } 323 try { 324 writeToSpec(spec, parseLong(value)); 325 } catch (NumberFormatException e) { 326 throw new IllegalArgumentException( 327 format("key %s value set to %s, must be integer", key, value), e); 328 } 329 } 330 } 331 332 /** Parse initialCapacity */ 333 private static final class InitialCapacityParser extends IntegerParser { 334 @Override 335 void writeToSpec(CacheBuilderSpec spec, int value) { 336 checkArgument( 337 spec.initialCapacity == null, 338 "initial capacity was already set to %s", 339 spec.initialCapacity); 340 spec.initialCapacity = value; 341 } 342 } 343 344 /** Parse maximumSize */ 345 private static final class MaximumSizeParser extends LongParser { 346 @Override 347 void writeToSpec(CacheBuilderSpec spec, long value) { 348 checkArgument( 349 spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize); 350 checkArgument( 351 spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight); 352 spec.maximumSize = value; 353 } 354 } 355 356 /** Parse maximumWeight */ 357 private static final class MaximumWeightParser extends LongParser { 358 @Override 359 void writeToSpec(CacheBuilderSpec spec, long value) { 360 checkArgument( 361 spec.maximumWeight == null, "maximum weight was already set to %s", spec.maximumWeight); 362 checkArgument( 363 spec.maximumSize == null, "maximum size was already set to %s", spec.maximumSize); 364 spec.maximumWeight = value; 365 } 366 } 367 368 /** Parse concurrencyLevel */ 369 private static final class ConcurrencyLevelParser extends IntegerParser { 370 @Override 371 void writeToSpec(CacheBuilderSpec spec, int value) { 372 checkArgument( 373 spec.concurrencyLevel == null, 374 "concurrency level was already set to %s", 375 spec.concurrencyLevel); 376 spec.concurrencyLevel = value; 377 } 378 } 379 380 /** Parse weakKeys */ 381 private static final class KeyStrengthParser implements ValueParser { 382 private final Strength strength; 383 384 KeyStrengthParser(Strength strength) { 385 this.strength = strength; 386 } 387 388 @Override 389 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 390 checkArgument(value == null, "key %s does not take values", key); 391 checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength); 392 spec.keyStrength = strength; 393 } 394 } 395 396 /** Parse weakValues and softValues */ 397 private static final class ValueStrengthParser implements ValueParser { 398 private final Strength strength; 399 400 ValueStrengthParser(Strength strength) { 401 this.strength = strength; 402 } 403 404 @Override 405 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 406 checkArgument(value == null, "key %s does not take values", key); 407 checkArgument( 408 spec.valueStrength == null, "%s was already set to %s", key, spec.valueStrength); 409 410 spec.valueStrength = strength; 411 } 412 } 413 414 /** Parse recordStats */ 415 private static final class RecordStatsParser implements ValueParser { 416 417 @Override 418 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 419 checkArgument(value == null, "recordStats does not take values"); 420 checkArgument(spec.recordStats == null, "recordStats already set"); 421 spec.recordStats = true; 422 } 423 } 424 425 /** Base class for parsing times with durations */ 426 private abstract static class DurationParser implements ValueParser { 427 abstract void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit); 428 429 @Override 430 public void parse(CacheBuilderSpec spec, String key, @Nullable String value) { 431 if (isNullOrEmpty(value)) { 432 throw new IllegalArgumentException("value of key " + key + " omitted"); 433 } 434 try { 435 char lastChar = value.charAt(value.length() - 1); 436 TimeUnit timeUnit; 437 switch (lastChar) { 438 case 'd': 439 timeUnit = DAYS; 440 break; 441 case 'h': 442 timeUnit = HOURS; 443 break; 444 case 'm': 445 timeUnit = MINUTES; 446 break; 447 case 's': 448 timeUnit = SECONDS; 449 break; 450 default: 451 throw new IllegalArgumentException( 452 format("key %s invalid unit: was %s, must end with one of [dhms]", key, value)); 453 } 454 455 long duration = parseLong(value.substring(0, value.length() - 1)); 456 writeToSpec(spec, duration, timeUnit); 457 } catch (NumberFormatException e) { 458 throw new IllegalArgumentException( 459 format("key %s value set to %s, must be integer", key, value)); 460 } 461 } 462 } 463 464 /** Parse expireAfterAccess */ 465 private static final class AccessDurationParser extends DurationParser { 466 @Override 467 void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) { 468 checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set"); 469 spec.accessExpirationDuration = duration; 470 spec.accessExpirationTimeUnit = unit; 471 } 472 } 473 474 /** Parse expireAfterWrite */ 475 private static final class WriteDurationParser extends DurationParser { 476 @Override 477 void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) { 478 checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set"); 479 spec.writeExpirationDuration = duration; 480 spec.writeExpirationTimeUnit = unit; 481 } 482 } 483 484 /** Parse refreshAfterWrite */ 485 private static final class RefreshDurationParser extends DurationParser { 486 @Override 487 void writeToSpec(CacheBuilderSpec spec, long duration, TimeUnit unit) { 488 checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set"); 489 spec.refreshDuration = duration; 490 spec.refreshTimeUnit = unit; 491 } 492 } 493 494 @FormatMethod 495 private static String format(String format, Object... args) { 496 return String.format(Locale.ROOT, format, args); 497 } 498}