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

ben-manes / caffeine / #5173

29 Dec 2025 05:27AM UTC coverage: 0.0% (-100.0%) from 100.0%
#5173

push

github

ben-manes
speed up development ci build

0 of 3838 branches covered (0.0%)

0 of 7869 relevant lines covered (0.0%)

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.UNSET_INT;
19
import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument;
20
import static com.github.benmanes.caffeine.cache.Caffeine.requireState;
21
import static java.util.Locale.US;
22
import static java.util.Objects.requireNonNull;
23

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

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

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

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

80
  final String specification;
81

82
  int initialCapacity = UNSET_INT;
×
83
  long maximumWeight = UNSET_INT;
×
84
  long maximumSize = UNSET_INT;
×
85
  boolean recordStats;
86

87
  @Nullable Strength keyStrength;
88
  @Nullable Strength valueStrength;
89
  @Nullable Duration expireAfterWrite;
90
  @Nullable Duration expireAfterAccess;
91
  @Nullable Duration refreshAfterWrite;
92

93
  private CaffeineSpec(String specification) {
×
94
    this.specification = requireNonNull(specification);
×
95

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

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

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

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

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

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

169
    configure(option, key, value);
×
170
  }
×
171

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

394
  /**
395
   * Returns a string representation for this {@code CaffeineSpec} instance. The form of this
396
   * representation is not guaranteed.
397
   */
398
  @Override
399
  public String toString() {
400
    return getClass().getSimpleName() + '{' + toParsableString() + '}';
×
401
  }
402
}
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