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

ben-manes / caffeine / #5594

06 Jul 2026 06:08PM UTC coverage: 99.713% (-0.2%) from 99.892%
#5594

push

github

ben-manes
Match ConcurrentHashMap for containsAll on the map views

keySet()/values().containsAll inherited AbstractCollection's loop,
which threw NullPointerException on a null element and iterated even
for a self-check. CHM and Guava both skip a null element (false) and
short-circuit containsAll(self) to true; Caffeine's removeAll and
entrySet were already lenient, leaving these two views the holdout.
Override containsAll across the bounded, unbounded, and async views.
The CaffeinatedGuava facade's override (a workaround for the old NPE)
is now redundant, so drop it and delegate to the view.

4106 of 4132 branches covered (99.37%)

42 of 42 new or added lines in 3 files covered. (100.0%)

17 existing lines in 5 files now uncovered.

8350 of 8374 relevant lines covered (99.71%)

1.0 hits per line

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

97.89
/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) {
1✔
92
    this.specification = requireNonNull(specification);
1✔
93

94
    @SuppressWarnings("StringSplitter")
95
    var options = specification.split(SPLIT_OPTIONS);
1✔
96
    for (String option : options) {
1✔
97
      parseOption(option.strip());
1✔
98
    }
99
  }
1✔
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();
1✔
108
    if (initialCapacity != null) {
1✔
109
      builder.initialCapacity(initialCapacity);
1✔
110
    }
111
    if (maximumSize != null) {
1✔
112
      builder.maximumSize(maximumSize);
1✔
113
    }
114
    if (maximumWeight != null) {
1✔
115
      builder.maximumWeight(maximumWeight);
1✔
116
    }
117
    if (keyStrength != null) {
1✔
118
      requireState(keyStrength == Strength.WEAK);
1✔
119
      builder.weakKeys();
1✔
120
    }
121
    if (valueStrength != null) {
1✔
122
      if (valueStrength == Strength.WEAK) {
1✔
123
        builder.weakValues();
1✔
124
      } else {
125
        builder.softValues();
1✔
126
      }
127
    }
128
    if (expireAfterWrite != null) {
1✔
129
      builder.expireAfterWrite(expireAfterWrite);
1✔
130
    }
131
    if (expireAfterAccess != null) {
1✔
132
      builder.expireAfterAccess(expireAfterAccess);
1✔
133
    }
134
    if (refreshAfterWrite != null) {
1✔
135
      builder.refreshAfterWrite(refreshAfterWrite);
1✔
136
    }
137
    if (recordStats) {
1✔
138
      builder.recordStats();
1✔
139
    }
140
    return builder;
1✔
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);
1✔
151
  }
152

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

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

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

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

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

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

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

224
  /** Configures the maximum weight. */
225
  void maximumWeight(String key, @Nullable String value) {
226
    requireArgument(maximumWeight == null,
1✔
227
        "maximum weight was already set to %,d", maximumWeight);
228
    requireArgument(maximumSize == null,
1✔
229
        "maximum size was already set to %,d", maximumSize);
230
    maximumWeight = parseLong(key, value);
1✔
231
  }
1✔
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");
1✔
236
    requireArgument(keyStrength == null, "weak keys was already set");
1✔
237
    keyStrength = Strength.WEAK;
1✔
238
  }
1✔
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);
1✔
243
    requireArgument(valueStrength == null, "%s was already set to %s", key, valueStrength);
1✔
244
    valueStrength = strength;
1✔
245
  }
1✔
246

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

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

259
  /** Configures refresh after write. */
260
  void refreshAfterWrite(String key, @Nullable String value) {
261
    requireArgument(refreshAfterWrite == null, "refreshAfterWrite was already set");
1✔
262
    refreshAfterWrite = parseDuration(key, value);
1✔
263
  }
1✔
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");
1✔
268
    requireArgument(!recordStats, "record stats was already set");
1✔
269
    recordStats = true;
1✔
270
  }
1✔
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);
1✔
275
    requireNonNull(value);
1✔
276
    try {
277
      return Integer.parseInt(normalizeNumericLiteral(value));
1✔
278
    } catch (NumberFormatException e) {
1✔
279
      throw new IllegalArgumentException(String.format(US,
1✔
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);
1✔
287
    requireNonNull(value);
1✔
288
    try {
289
      return Long.parseLong(normalizeNumericLiteral(value));
1✔
290
    } catch (NumberFormatException e) {
1✔
291
      throw new IllegalArgumentException(String.format(US,
1✔
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("-_")
1✔
299
        || value.startsWith("_") || value.endsWith("_");
1✔
300
    return invalid ? value : value.replace("_", "");
1✔
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);
1!
306
    requireNonNull(value);
1✔
307

308
    boolean isIsoFormat = value.contains("p") || value.contains("P");
1✔
309
    Duration duration = isIsoFormat
1✔
310
        ? parseIsoDuration(key, value)
1✔
311
        : parseSimpleDuration(key, value);
1✔
312
    requireArgument(!duration.isNegative(),
1✔
313
        "key %s invalid format; was %s, but the duration cannot be negative", key, value);
314
    return duration;
1✔
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);
1✔
UNCOV
321
    } catch (DateTimeParseException e) {
×
UNCOV
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));
1✔
330
    TimeUnit unit = parseTimeUnit(key, value);
1✔
331
    return Duration.ofNanos(unit.toNanos(duration));
1✔
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);
1!
338
    @SuppressWarnings("null")
339
    char lastChar = Character.toLowerCase(value.charAt(value.length() - 1));
1✔
340
    switch (lastChar) {
1!
341
      case 'd':
342
        return TimeUnit.DAYS;
1✔
343
      case 'h':
344
        return TimeUnit.HOURS;
1✔
345
      case 'm':
346
        return TimeUnit.MINUTES;
1✔
347
      case 's':
348
        return TimeUnit.SECONDS;
1✔
349
      default:
UNCOV
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) {
1✔
358
      return true;
1✔
359
    } else if (!(o instanceof CaffeineSpec)) {
1✔
360
      return false;
1✔
361
    }
362
    var spec = (CaffeineSpec) o;
1✔
363
    return Objects.equals(refreshAfterWrite, spec.refreshAfterWrite)
1!
364
        && Objects.equals(expireAfterAccess, spec.expireAfterAccess)
1✔
365
        && Objects.equals(expireAfterWrite, spec.expireAfterWrite)
1✔
366
        && Objects.equals(initialCapacity, spec.initialCapacity)
1✔
367
        && Objects.equals(maximumWeight, spec.maximumWeight)
1✔
368
        && Objects.equals(maximumSize, spec.maximumSize)
1✔
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,
1✔
377
        recordStats, expireAfterWrite, expireAfterAccess, refreshAfterWrite);
1✔
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;
1✔
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() + '}';
1✔
399
  }
400
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc