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

ben-manes / caffeine / #5588

05 Jul 2026 04:11AM UTC coverage: 99.772% (-0.1%) from 99.904%
#5588

push

github

ben-manes
Make the async asMap() view equals reflexive

AsyncAsMapView.equals delegated straight to cache().equals(o), so a
self-compare skipped the o == this short-circuit its three sibling
views have. cache().equals then returns false whenever it iterates
an expired-but-unreaped (or collected) entry, so view.equals(view)
could break Object.equals reflexivity. Prepend the identity check.

4061 of 4086 branches covered (99.39%)

1 of 1 new or added line in 1 file covered. (100.0%)

11 existing lines in 4 files now uncovered.

8314 of 8333 relevant lines covered (99.77%)

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