• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ben-manes / caffeine / #5707

02 Aug 2026 12:45AM UTC coverage: 0.106% (-99.9%) from 100.0%
#5707

push

github

ben-manes
fix wikibench trace reader after overly strict audit fixes

0 of 4235 branches covered (0.0%)

9 of 8528 relevant lines covered (0.11%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java
1
/*
2
 * Copyright 2016 Ben Manes. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package com.github.benmanes.caffeine.cache;
17

18
import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument;
19
import static com.github.benmanes.caffeine.cache.Caffeine.requireState;
20
import static java.util.Locale.US;
21
import static java.util.Objects.requireNonNull;
22

23
import java.time.Duration;
24
import java.time.format.DateTimeParseException;
25
import java.util.Objects;
26
import java.util.concurrent.TimeUnit;
27

28
import org.jspecify.annotations.NullMarked;
29
import org.jspecify.annotations.Nullable;
30

31
import com.github.benmanes.caffeine.cache.Caffeine.Strength;
32

33
/**
34
 * A specification of a {@link Caffeine} builder configuration.
35
 * <p>
36
 * {@code CaffeineSpec} supports parsing configuration from a string, which makes it especially
37
 * useful for command-line configuration of a {@code Caffeine} builder.
38
 * <p>
39
 * The string syntax is a series of comma-separated keys or key-value pairs, each corresponding to a
40
 * {@code Caffeine} builder method.
41
 * <ul>
42
 *   <li>{@code initialCapacity=[integer]}: sets {@link Caffeine#initialCapacity}.
43
 *   <li>{@code maximumSize=[long]}: sets {@link Caffeine#maximumSize}.
44
 *   <li>{@code maximumWeight=[long]}: sets {@link Caffeine#maximumWeight}.
45
 *   <li>{@code expireAfterAccess=[duration]}: sets {@link Caffeine#expireAfterAccess}.
46
 *   <li>{@code expireAfterWrite=[duration]}: sets {@link Caffeine#expireAfterWrite}.
47
 *   <li>{@code refreshAfterWrite=[duration]}: sets {@link Caffeine#refreshAfterWrite}.
48
 *   <li>{@code weakKeys}: sets {@link Caffeine#weakKeys}.
49
 *   <li>{@code weakValues}: sets {@link Caffeine#weakValues}.
50
 *   <li>{@code softValues}: sets {@link Caffeine#softValues}.
51
 *   <li>{@code recordStats}: sets {@link Caffeine#recordStats}.
52
 * </ul>
53
 * <p>
54
 * Durations are represented as either an ISO-8601 string using {@link Duration#parse(CharSequence)}
55
 * or by an integer followed by one of "d", "h", "m", or "s", representing days, hours, minutes, or
56
 * seconds, respectively. There is currently no short syntax to request durations in milliseconds,
57
 * microseconds, or nanoseconds.
58
 * <p>
59
 * Whitespace before and after commas and equal signs is ignored. Keys may not be repeated; it is
60
 * also illegal to use the following pairs of keys in a single value:
61
 * <ul>
62
 *   <li>{@code maximumSize} and {@code maximumWeight}
63
 *   <li>{@code weakValues} and {@code softValues}
64
 * </ul>
65
 * <p>
66
 * {@code CaffeineSpec} does not support configuring {@code Caffeine} methods with non-value
67
 * parameters. These must be configured in code.
68
 * <p>
69
 * A new {@code Caffeine} builder can be instantiated from a {@code CaffeineSpec} using
70
 * {@link Caffeine#from(CaffeineSpec)} or {@link Caffeine#from(String)}.
71
 *
72
 * @author ben.manes@gmail.com (Ben Manes)
73
 */
74
@NullMarked
75
public final class CaffeineSpec {
76
  static final String SPLIT_OPTIONS = ",";
77
  static final String SPLIT_KEY_VALUE = "=";
78

79
  final String specification;
80

81
  boolean recordStats;
82
  @Nullable Long maximumSize;
83
  @Nullable Long maximumWeight;
84
  @Nullable Strength keyStrength;
85
  @Nullable Strength valueStrength;
86
  @Nullable Integer initialCapacity;
87
  @Nullable Duration expireAfterWrite;
88
  @Nullable Duration expireAfterAccess;
89
  @Nullable Duration refreshAfterWrite;
90

91
  private CaffeineSpec(String specification) {
×
92
    this.specification = requireNonNull(specification);
×
93

94
    @SuppressWarnings("StringSplitter")
95
    var options = specification.split(SPLIT_OPTIONS);
×
96
    for (String option : options) {
×
97
      parseOption(option.strip());
×
98
    }
99
  }
×
100

101
  /**
102
   * Returns a {@link Caffeine} builder configured according to this specification.
103
   *
104
   * @return a builder configured to the specification
105
   */
106
  Caffeine<Object, Object> toBuilder() {
107
    var builder = Caffeine.newBuilder();
×
108
    if (initialCapacity != null) {
×
109
      builder.initialCapacity(initialCapacity);
×
110
    }
111
    if (maximumSize != null) {
×
112
      builder.maximumSize(maximumSize);
×
113
    }
114
    if (maximumWeight != null) {
×
115
      builder.maximumWeight(maximumWeight);
×
116
    }
117
    if (keyStrength != null) {
×
118
      requireState(keyStrength == Strength.WEAK);
×
119
      builder.weakKeys();
×
120
    }
121
    if (valueStrength != null) {
×
122
      if (valueStrength == Strength.WEAK) {
×
123
        builder.weakValues();
×
124
      } else {
125
        builder.softValues();
×
126
      }
127
    }
128
    if (expireAfterWrite != null) {
×
129
      builder.expireAfterWrite(expireAfterWrite);
×
130
    }
131
    if (expireAfterAccess != null) {
×
132
      builder.expireAfterAccess(expireAfterAccess);
×
133
    }
134
    if (refreshAfterWrite != null) {
×
135
      builder.refreshAfterWrite(refreshAfterWrite);
×
136
    }
137
    if (recordStats) {
×
138
      builder.recordStats();
×
139
    }
140
    return builder;
×
141
  }
142

143
  /**
144
   * Creates a CaffeineSpec from a string.
145
   *
146
   * @param specification the string form
147
   * @return the parsed specification
148
   */
149
  public static CaffeineSpec parse(String specification) {
150
    return new CaffeineSpec(specification);
×
151
  }
152

153
  /** Parses and applies the configuration option. */
154
  void parseOption(String option) {
155
    if (option.isEmpty()) {
×
156
      return;
×
157
    }
158

159
    @SuppressWarnings("StringSplitter")
160
    String[] keyAndValue = option.split(SPLIT_KEY_VALUE, 3);
×
161
    requireArgument(keyAndValue.length <= 2,
×
162
        "key-value pair %s with more than one equals sign", option);
163

164
    String key = keyAndValue[0].strip();
×
165
    String value = (keyAndValue.length == 1) ? null : keyAndValue[1].strip();
×
166

167
    configure(option, key, value);
×
168
  }
×
169

170
  /** Configures the setting. */
171
  void configure(String option, String key, @Nullable String value) {
172
    switch (key) {
×
173
      case "initialCapacity":
174
        initialCapacity(key, value);
×
175
        return;
×
176
      case "maximumSize":
177
        maximumSize(key, value);
×
178
        return;
×
179
      case "maximumWeight":
180
        maximumWeight(key, value);
×
181
        return;
×
182
      case "weakKeys":
183
        weakKeys(value);
×
184
        return;
×
185
      case "weakValues":
186
        valueStrength(key, value, Strength.WEAK);
×
187
        return;
×
188
      case "softValues":
189
        valueStrength(key, value, Strength.SOFT);
×
190
        return;
×
191
      case "expireAfterAccess":
192
        expireAfterAccess(key, value);
×
193
        return;
×
194
      case "expireAfterWrite":
195
        expireAfterWrite(key, value);
×
196
        return;
×
197
      case "refreshAfterWrite":
198
        refreshAfterWrite(key, value);
×
199
        return;
×
200
      case "recordStats":
201
        recordStats(value);
×
202
        return;
×
203
      default:
204
        throw new IllegalArgumentException("Invalid option " + option);
×
205
    }
206
  }
207

208
  /** Configures the initial capacity. */
209
  void initialCapacity(String key, @Nullable String value) {
210
    requireArgument(initialCapacity == null,
×
211
        "initial capacity was already set to %,d", initialCapacity);
212
    initialCapacity = parseInt(key, value);
×
213
  }
×
214

215
  /** Configures the maximum size. */
216
  void maximumSize(String key, @Nullable String value) {
217
    requireArgument(maximumSize == null,
×
218
        "maximum size was already set to %,d", maximumSize);
219
    requireArgument(maximumWeight == null,
×
220
        "maximum weight was already set to %,d", maximumWeight);
221
    maximumSize = parseLong(key, value);
×
222
  }
×
223

224
  /** Configures the maximum weight. */
225
  void maximumWeight(String key, @Nullable String value) {
226
    requireArgument(maximumWeight == null,
×
227
        "maximum weight was already set to %,d", maximumWeight);
228
    requireArgument(maximumSize == null,
×
229
        "maximum size was already set to %,d", maximumSize);
230
    maximumWeight = parseLong(key, value);
×
231
  }
×
232

233
  /** Configures the keys as weak references. */
234
  void weakKeys(@Nullable String value) {
235
    requireArgument(value == null, "weak keys does not take a value");
×
236
    requireArgument(keyStrength == null, "weak keys was already set");
×
237
    keyStrength = Strength.WEAK;
×
238
  }
×
239

240
  /** Configures the value as weak or soft references. */
241
  void valueStrength(String key, @Nullable String value, Strength strength) {
242
    requireArgument(value == null, "%s does not take a value", key);
×
243
    requireArgument(valueStrength == null, "%s was already set to %s", key, valueStrength);
×
244
    valueStrength = strength;
×
245
  }
×
246

247
  /** Configures expire after access. */
248
  void expireAfterAccess(String key, @Nullable String value) {
249
    requireArgument(expireAfterAccess == null, "expireAfterAccess was already set");
×
250
    expireAfterAccess = parseDuration(key, value);
×
251
  }
×
252

253
  /** Configures expire after write. */
254
  void expireAfterWrite(String key, @Nullable String value) {
255
    requireArgument(expireAfterWrite == null, "expireAfterWrite was already set");
×
256
    expireAfterWrite = parseDuration(key, value);
×
257
  }
×
258

259
  /** Configures refresh after write. */
260
  void refreshAfterWrite(String key, @Nullable String value) {
261
    requireArgument(refreshAfterWrite == null, "refreshAfterWrite was already set");
×
262
    refreshAfterWrite = parseDuration(key, value);
×
263
  }
×
264

265
  /** Configures the value as weak or soft references. */
266
  void recordStats(@Nullable String value) {
267
    requireArgument(value == null, "record stats does not take a value");
×
268
    requireArgument(!recordStats, "record stats was already set");
×
269
    recordStats = true;
×
270
  }
×
271

272
  /** Returns a parsed int value. */
273
  static int parseInt(String key, @Nullable String value) {
274
    requireArgument((value != null) && !value.isEmpty(), "value of key %s was omitted", key);
×
275
    requireNonNull(value);
×
276
    try {
277
      return Integer.parseInt(normalizeNumericLiteral(value));
×
278
    } catch (NumberFormatException e) {
×
279
      throw new IllegalArgumentException(String.format(US,
×
280
          "key %s value was set to %s, must be an integer", key, value), e);
281
    }
282
  }
283

284
  /** Returns a parsed long value. */
285
  static long parseLong(String key, @Nullable String value) {
286
    requireArgument((value != null) && !value.isEmpty(), "value of key %s was omitted", key);
×
287
    requireNonNull(value);
×
288
    try {
289
      return Long.parseLong(normalizeNumericLiteral(value));
×
290
    } catch (NumberFormatException e) {
×
291
      throw new IllegalArgumentException(String.format(US,
×
292
          "key %s value was set to %s, must be a long", key, value), e);
293
    }
294
  }
295

296
  /** Returns the value after adjusting for underscores in a numeric literal. */
297
  static String normalizeNumericLiteral(String value) {
298
    boolean invalid = value.startsWith("+_") || value.startsWith("-_")
×
299
        || value.startsWith("_") || value.endsWith("_");
×
300
    return invalid ? value : value.replace("_", "");
×
301
  }
302

303
  /** Returns a parsed duration value. */
304
  static Duration parseDuration(String key, @Nullable String value) {
305
    requireArgument((value != null) && !value.isEmpty(), "value of key %s omitted", key);
×
306
    requireNonNull(value);
×
307

308
    boolean isIsoFormat = value.contains("p") || value.contains("P");
×
309
    Duration duration = isIsoFormat
×
310
        ? parseIsoDuration(key, value)
×
311
        : parseSimpleDuration(key, value);
×
312
    requireArgument(!duration.isNegative(),
×
313
        "key %s invalid format; was %s, but the duration cannot be negative", key, value);
314
    return duration;
×
315
  }
316

317
  /** Returns a parsed duration using the ISO-8601 format. */
318
  static Duration parseIsoDuration(String key, String value) {
319
    try {
320
      return Duration.parse(value);
×
321
    } catch (DateTimeParseException e) {
×
322
      throw new IllegalArgumentException(String.format(US,
×
323
          "key %s invalid format; was %s, but the duration cannot be parsed", key, value), e);
324
    }
325
  }
326

327
  /** Returns a parsed duration using the simple time unit format. */
328
  static Duration parseSimpleDuration(String key, String value) {
329
    long duration = parseLong(key, value.substring(0, value.length() - 1));
×
330
    TimeUnit unit = parseTimeUnit(key, value);
×
331
    return Duration.ofNanos(unit.toNanos(duration));
×
332
  }
333

334
  /** Returns a parsed {@link TimeUnit} value. */
335
  @SuppressWarnings({"ConstantValue", "StatementSwitchToExpressionSwitch"})
336
  static TimeUnit parseTimeUnit(String key, String value) {
337
    requireArgument((value != null) && !value.isEmpty(), "value of key %s omitted", key);
×
338
    @SuppressWarnings("null")
339
    char lastChar = Character.toLowerCase(value.charAt(value.length() - 1));
×
340
    switch (lastChar) {
×
341
      case 'd':
342
        return TimeUnit.DAYS;
×
343
      case 'h':
344
        return TimeUnit.HOURS;
×
345
      case 'm':
346
        return TimeUnit.MINUTES;
×
347
      case 's':
348
        return TimeUnit.SECONDS;
×
349
      default:
350
        throw new IllegalArgumentException(String.format(US,
×
351
            "key %s invalid format; was %s, must end with one of [dDhHmMsS]", key, value));
352
    }
353
  }
354

355
  @Override
356
  public boolean equals(@Nullable Object o) {
357
    if (this == o) {
×
358
      return true;
×
359
    } else if (!(o instanceof CaffeineSpec)) {
×
360
      return false;
×
361
    }
362
    var spec = (CaffeineSpec) o;
×
363
    return Objects.equals(refreshAfterWrite, spec.refreshAfterWrite)
×
364
        && Objects.equals(expireAfterAccess, spec.expireAfterAccess)
×
365
        && Objects.equals(expireAfterWrite, spec.expireAfterWrite)
×
366
        && Objects.equals(initialCapacity, spec.initialCapacity)
×
367
        && Objects.equals(maximumWeight, spec.maximumWeight)
×
368
        && Objects.equals(maximumSize, spec.maximumSize)
×
369
        && (valueStrength == spec.valueStrength)
370
        && (keyStrength == spec.keyStrength)
371
        && (recordStats == spec.recordStats);
372
  }
373

374
  @Override
375
  public int hashCode() {
376
    return Objects.hash(initialCapacity, maximumSize, maximumWeight, keyStrength, valueStrength,
×
377
        recordStats, expireAfterWrite, expireAfterAccess, refreshAfterWrite);
×
378
  }
379

380
  /**
381
   * Returns a string representation that can be used to parse an equivalent {@code CaffeineSpec}.
382
   * The order and form of this representation is not guaranteed, except that parsing its output
383
   * will produce a {@code CaffeineSpec} equal to this instance.
384
   *
385
   * @return a string representation of this specification that can be parsed into a
386
   *         {@code CaffeineSpec}
387
   */
388
  public String toParsableString() {
389
    return specification;
×
390
  }
391

392
  /**
393
   * Returns a string representation for this {@code CaffeineSpec} instance. The form of this
394
   * representation is not guaranteed.
395
   */
396
  @Override
397
  public String toString() {
398
    return getClass().getSimpleName() + '{' + toParsableString() + '}';
×
399
  }
400
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc