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

ben-manes / caffeine / #5651

18 Jul 2026 03:26AM UTC coverage: 66.303% (-33.7%) from 100.0%
#5651

push

github

ben-manes
dependency updates

2809 of 4164 branches covered (67.46%)

5584 of 8422 relevant lines covered (66.3%)

0.66 hits per line

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

0.0
/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java
1
/*
2
 * Copyright 2015 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.jcache.configuration;
17

18
import static java.util.Objects.requireNonNull;
19
import static java.util.concurrent.TimeUnit.MILLISECONDS;
20
import static java.util.concurrent.TimeUnit.NANOSECONDS;
21

22
import java.io.File;
23
import java.lang.System.Logger;
24
import java.lang.System.Logger.Level;
25
import java.net.MalformedURLException;
26
import java.net.URI;
27
import java.util.Collections;
28
import java.util.LinkedHashSet;
29
import java.util.Objects;
30
import java.util.Optional;
31
import java.util.OptionalLong;
32
import java.util.Set;
33
import java.util.concurrent.atomic.AtomicReference;
34
import java.util.function.Supplier;
35

36
import javax.cache.CacheManager;
37
import javax.cache.configuration.Factory;
38
import javax.cache.configuration.FactoryBuilder;
39
import javax.cache.configuration.MutableCacheEntryListenerConfiguration;
40
import javax.cache.event.CacheEntryEventFilter;
41
import javax.cache.event.CacheEntryListener;
42
import javax.cache.expiry.Duration;
43
import javax.cache.expiry.EternalExpiryPolicy;
44
import javax.cache.expiry.ExpiryPolicy;
45

46
import org.jspecify.annotations.NullMarked;
47
import org.jspecify.annotations.Nullable;
48

49
import com.github.benmanes.caffeine.jcache.expiry.JCacheExpiryPolicy;
50
import com.google.errorprone.annotations.Var;
51
import com.typesafe.config.Config;
52
import com.typesafe.config.ConfigException;
53
import com.typesafe.config.ConfigFactory;
54
import com.typesafe.config.ConfigParseOptions;
55
import com.typesafe.config.ConfigSyntax;
56

57
import jakarta.inject.Inject;
58

59
/**
60
 * Static utility methods pertaining to externalized {@link CaffeineConfiguration} entries using the
61
 * Typesafe Config library.
62
 *
63
 * @author ben.manes@gmail.com (Ben Manes)
64
 */
65
@NullMarked
66
public final class TypesafeConfigurator {
67
  static final Logger logger = System.getLogger(TypesafeConfigurator.class.getName());
×
68

69
  /** Sections nested under {@code caffeine.jcache} that are reserved and not cache definitions. */
70
  static final Set<String> RESERVED_NAMES = Set.of("default", "listeners");
×
71

72
  static final AtomicReference<ConfigSource> configSource =
×
73
      new AtomicReference<>(TypesafeConfigurator::resolveConfig);
74
  static final AtomicReference<FactoryCreator> factoryCreator =
×
75
      new AtomicReference<>(FactoryBuilder::factoryOf);
76

77
  private TypesafeConfigurator() {}
78

79
  /**
80
   * Retrieves the names of the caches defined in the configuration resource.
81
   *
82
   * @param config the configuration resource
83
   * @return the names of the configured caches
84
   */
85
  public static Set<String> cacheNames(Config config) {
86
    if (!config.hasPath("caffeine.jcache")) {
×
87
      return Collections.emptySet();
×
88
    }
89
    var names = new LinkedHashSet<>(config.getObject("caffeine.jcache").keySet());
×
90
    names.removeAll(RESERVED_NAMES);
×
91
    return Collections.unmodifiableSet(names);
×
92
  }
93

94
  /**
95
   * Retrieves the default cache settings from the configuration resource.
96
   *
97
   * @param config the configuration resource
98
   * @param <K> the type of keys maintained the cache
99
   * @param <V> the type of cached values
100
   * @return the default configuration for a cache
101
   */
102
  public static <K, V> CaffeineConfiguration<K, V> defaults(Config config) {
103
    return new Configurator<K, V>(config, "default").configure();
×
104
  }
105

106
  /**
107
   * Retrieves the cache's settings from the configuration resource if defined.
108
   *
109
   * @param config the configuration resource
110
   * @param cacheName the name of the cache
111
   * @param <K> the type of keys maintained the cache
112
   * @param <V> the type of cached values
113
   * @return the configuration for the cache
114
   */
115
  public static <K, V> Optional<CaffeineConfiguration<K, V>> from(Config config, String cacheName) {
116
    try {
117
      requireNonNull(cacheName);
×
118
      return (!RESERVED_NAMES.contains(cacheName) && config.hasPath("caffeine.jcache." + cacheName))
×
119
          ? Optional.of(new Configurator<K, V>(config, cacheName).configure())
×
120
          : Optional.empty();
×
121
    } catch (ConfigException.BadPath e) {
×
122
      logger.log(Level.WARNING, "Failed to load cache configuration", e);
×
123
      return Optional.empty();
×
124
    }
125
  }
126

127
  /**
128
   * Specifies how {@link Factory} instances are created for a given class name. The default
129
   * strategy uses {@link Class#newInstance()} and requires the class has a no-args constructor.
130
   *
131
   * @param factoryCreator the strategy for creating a factory
132
   */
133
  @Inject
134
  @SuppressWarnings({"deprecation", "UnnecessarilyVisible"})
135
  public static void setFactoryCreator(FactoryCreator factoryCreator) {
136
    TypesafeConfigurator.factoryCreator.set(requireNonNull(factoryCreator));
×
137
  }
×
138

139
  /** Returns the strategy for how factory instances are created. */
140
  public static FactoryCreator factoryCreator() {
141
    return requireNonNull(factoryCreator.get());
×
142
  }
143

144
  /**
145
   * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri
146
   * provided by {@link CacheManager#getURI()} as an optional override location to parse from a
147
   * file system or classpath resource, or else returns {@link ConfigFactory#load(ClassLoader)}.
148
   * The configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed
149
   * that the source caches it as needed.
150
   *
151
   * @param configSource the strategy for loading the configuration
152
   */
153
  public static void setConfigSource(Supplier<Config> configSource) {
154
    requireNonNull(configSource);
×
155
    setConfigSource((uri, classloader) -> configSource.get());
×
156
  }
×
157

158
  /**
159
   * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri
160
   * provided by {@link CacheManager#getURI()} as an optional override location to parse from a
161
   * file system or classpath resource, or else returns {@link ConfigFactory#load(ClassLoader)}.
162
   * The configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed
163
   * that the source caches it as needed.
164
   *
165
   * @param configSource the strategy for loading the configuration from a URI
166
   */
167
  public static void setConfigSource(ConfigSource configSource) {
168
    TypesafeConfigurator.configSource.set(requireNonNull(configSource));
×
169
  }
×
170

171
  /** Returns the strategy for loading the configuration. */
172
  public static ConfigSource configSource() {
173
    return requireNonNull(configSource.get());
×
174
  }
175

176
  /** Returns the configuration by applying the default strategy. */
177
  private static Config resolveConfig(URI uri, ClassLoader classloader) {
178
    requireNonNull(uri);
×
179
    requireNonNull(classloader);
×
180
    var options = ConfigParseOptions.defaults()
×
181
        .setClassLoader(classloader)
×
182
        .setAllowMissing(false);
×
183
    if ((uri.getScheme() != null) && uri.getScheme().equalsIgnoreCase("file")) {
×
184
      return ConfigFactory.defaultOverrides(classloader)
×
185
          .withFallback(ConfigFactory.parseFile(new File(uri), options))
×
186
          .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
187
          .resolve();
×
188
    } else if ((uri.getScheme() != null) && uri.getScheme().equalsIgnoreCase("jar")) {
×
189
      try {
190
        return ConfigFactory.defaultOverrides(classloader)
×
191
            .withFallback(ConfigFactory.parseURL(uri.toURL(), options))
×
192
            .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
193
            .resolve();
×
194
      } catch (MalformedURLException e) {
×
195
        throw new ConfigException.BadPath(uri.toString(), "Failed to load cache configuration", e);
×
196
      }
197
    } else if (isResource(uri)) {
×
198
      return ConfigFactory.defaultOverrides(classloader)
×
199
          .withFallback(ConfigFactory.parseResources(uri.getSchemeSpecificPart(), options))
×
200
          .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
201
          .resolve();
×
202
    }
203
    return ConfigFactory.load(classloader);
×
204
  }
205

206
  /** Returns if the uri is a file or classpath resource. */
207
  private static boolean isResource(URI uri) {
208
    if ((uri.getScheme() != null) && !uri.getScheme().equalsIgnoreCase("classpath")) {
×
209
      return false;
×
210
    }
211
    var path = uri.getSchemeSpecificPart();
×
212
    int dotIndex = path.lastIndexOf('.');
×
213
    if (dotIndex != -1) {
×
214
      var extension = path.substring(dotIndex + 1);
×
215
      for (var format : ConfigSyntax.values()) {
×
216
        if (format.toString().equalsIgnoreCase(extension)) {
×
217
          return true;
×
218
        }
219
      }
220
    }
221
    return false;
×
222
  }
223

224
  /** A one-shot builder for creating a configuration instance. */
225
  static final class Configurator<K, V> {
226
    final CaffeineConfiguration<K, V> configuration;
227
    final Config customized;
228
    final Config merged;
229
    final Config root;
230

231
    Configurator(Config config, String cacheName) {
×
232
      this.root = requireNonNull(config);
×
233
      this.configuration = new CaffeineConfiguration<>();
×
234
      this.customized = root.getConfig("caffeine.jcache." + requireNonNull(cacheName));
×
235
      this.merged = customized.withFallback(root.getConfig("caffeine.jcache.default"));
×
236
    }
×
237

238
    /** Returns a configuration built from the external settings. */
239
    CaffeineConfiguration<K, V> configure() {
240
      addKeyValueTypes();
×
241
      addStoreByValue();
×
242
      addExecutor();
×
243
      addScheduler();
×
244
      addListeners();
×
245
      addReadThrough();
×
246
      addWriteThrough();
×
247
      addMonitoring();
×
248
      addLazyExpiration();
×
249
      addEagerExpiration();
×
250
      addRefresh();
×
251
      addMaximum();
×
252

253
      return configuration;
×
254
    }
255

256
    /** Adds the key and value class types. */
257
    private void addKeyValueTypes() {
258
      @Var ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
×
259
      if (classLoader == null) {
×
260
        classLoader = getClass().getClassLoader();
×
261
      }
262
      try {
263
        @SuppressWarnings("unchecked")
264
        var keyType = (Class<K>) Class.forName(merged.getString("key-type"), true, classLoader);
×
265
        @SuppressWarnings("unchecked")
266
        var valueType = (Class<V>) Class.forName(merged.getString("value-type"), true, classLoader);
×
267
        configuration.setTypes(keyType, valueType);
×
268
      } catch (ClassNotFoundException e) {
×
269
        throw new IllegalStateException(e);
×
270
      }
×
271
    }
×
272

273
    /** Adds the store-by-value settings. */
274
    private void addStoreByValue() {
275
      configuration.setStoreByValue(merged.getBoolean("store-by-value.enabled"));
×
276
      if (isSet("store-by-value.strategy")) {
×
277
        configuration.setCopierFactory(factoryCreator().factoryOf(
×
278
            merged.getString("store-by-value.strategy")));
×
279
      }
280
    }
×
281

282
    /** Adds the executor settings. */
283
    public void addExecutor() {
284
      if (isSet("executor")) {
×
285
        configuration.setExecutorFactory(factoryCreator()
×
286
            .factoryOf(merged.getString("executor")));
×
287
      }
288
    }
×
289

290
    /** Adds the scheduler settings. */
291
    public void addScheduler() {
292
      if (isSet("scheduler")) {
×
293
        configuration.setSchedulerFactory(factoryCreator()
×
294
            .factoryOf(merged.getString("scheduler")));
×
295
      }
296
    }
×
297

298
    /** Adds the entry listeners settings. */
299
    private void addListeners() {
300
      for (String path : merged.getStringList("listeners")) {
×
301
        Config listener = root.getConfig(path);
×
302

303
        Factory<? extends CacheEntryListener<? super K, ? super V>> listenerFactory =
304
            factoryCreator().factoryOf(listener.getString("class"));
×
305
        @Var Factory<? extends CacheEntryEventFilter<? super K, ? super V>> filterFactory = null;
×
306
        if (listener.hasPath("filter")) {
×
307
          filterFactory = factoryCreator().factoryOf(listener.getString("filter"));
×
308
        }
309
        boolean oldValueRequired = listener.getBoolean("old-value-required");
×
310
        boolean synchronous = listener.getBoolean("synchronous");
×
311
        configuration.addCacheEntryListenerConfiguration(
×
312
            new MutableCacheEntryListenerConfiguration<>(
313
                listenerFactory, filterFactory, oldValueRequired, synchronous));
314
      }
×
315
    }
×
316

317
    /** Adds the read through settings. */
318
    private void addReadThrough() {
319
      configuration.setReadThrough(merged.getBoolean("read-through.enabled"));
×
320
      if (isSet("read-through.loader")) {
×
321
        configuration.setCacheLoaderFactory(factoryCreator().factoryOf(
×
322
            merged.getString("read-through.loader")));
×
323
      }
324
    }
×
325

326
    /** Adds the write-through settings. */
327
    private void addWriteThrough() {
328
      configuration.setWriteThrough(merged.getBoolean("write-through.enabled"));
×
329
      if (isSet("write-through.writer")) {
×
330
        configuration.setCacheWriterFactory(factoryCreator().factoryOf(
×
331
            merged.getString("write-through.writer")));
×
332
      }
333
    }
×
334

335
    /** Adds the monitoring settings. */
336
    private void addMonitoring() {
337
      configuration.setNativeStatisticsEnabled(merged.getBoolean("monitoring.native-statistics"));
×
338
      configuration.setStatisticsEnabled(merged.getBoolean("monitoring.statistics"));
×
339
      configuration.setManagementEnabled(merged.getBoolean("monitoring.management"));
×
340
    }
×
341

342
    /** Adds the JCache specification's lazy expiration settings. */
343
    public void addLazyExpiration() {
344
      Duration creation = getDurationFor("policy.lazy-expiration.creation");
×
345
      Duration update = getDurationFor("policy.lazy-expiration.update");
×
346
      Duration access = getDurationFor("policy.lazy-expiration.access");
×
347
      requireNonNull(creation, "policy.lazy-expiration.creation may not be null");
×
348

349
      boolean eternal = Objects.equals(creation, Duration.ETERNAL)
×
350
          && Objects.equals(update, Duration.ETERNAL)
×
351
          && Objects.equals(access, Duration.ETERNAL);
×
352
      Factory<? extends ExpiryPolicy> factory = eternal
×
353
          ? EternalExpiryPolicy.factoryOf()
×
354
          : FactoryBuilder.factoryOf(new JCacheExpiryPolicy(creation, update, access));
×
355
      configuration.setExpiryPolicyFactory(factory);
×
356
    }
×
357

358
    /** Returns the duration for the expiration time. */
359
    private @Nullable Duration getDurationFor(String path) {
360
      if (!isSet(path)) {
×
361
        return null;
×
362
      }
363
      if (merged.getString(path).equalsIgnoreCase("eternal")) {
×
364
        return Duration.ETERNAL;
×
365
      }
366
      long millis = merged.getDuration(path, MILLISECONDS);
×
367
      return new Duration(MILLISECONDS, millis);
×
368
    }
369

370
    /** Adds the Caffeine eager expiration settings. */
371
    public void addEagerExpiration() {
372
      if (isSet("policy.eager-expiration.after-write")) {
×
373
        long nanos = merged.getDuration("policy.eager-expiration.after-write", NANOSECONDS);
×
374
        configuration.setExpireAfterWrite(OptionalLong.of(nanos));
×
375
      }
376
      if (isSet("policy.eager-expiration.after-access")) {
×
377
        long nanos = merged.getDuration("policy.eager-expiration.after-access", NANOSECONDS);
×
378
        configuration.setExpireAfterAccess(OptionalLong.of(nanos));
×
379
      }
380
      if (isSet("policy.eager-expiration.variable")) {
×
381
        configuration.setExpiryFactory(Optional.of(factoryCreator().factoryOf(
×
382
            merged.getString("policy.eager-expiration.variable"))));
×
383
      }
384
    }
×
385

386
    /** Adds the Caffeine refresh settings. */
387
    public void addRefresh() {
388
      if (isSet("policy.refresh.after-write")) {
×
389
        long nanos = merged.getDuration("policy.refresh.after-write", NANOSECONDS);
×
390
        configuration.setRefreshAfterWrite(OptionalLong.of(nanos));
×
391
      }
392
    }
×
393

394
    /** Adds the maximum size and weight bounding settings. */
395
    private void addMaximum() {
396
      if (isSet("policy.maximum.size")) {
×
397
        configuration.setMaximumSize(OptionalLong.of(merged.getLong("policy.maximum.size")));
×
398
      }
399
      if (isSet("policy.maximum.weight")) {
×
400
        configuration.setMaximumWeight(OptionalLong.of(merged.getLong("policy.maximum.weight")));
×
401
      }
402
      if (isSet("policy.maximum.weigher")) {
×
403
        configuration.setWeigherFactory(Optional.of(
×
404
            factoryCreator().factoryOf(merged.getString("policy.maximum.weigher"))));
×
405
      }
406
    }
×
407

408
    /** Returns if the value is present (not unset by the cache configuration). */
409
    private boolean isSet(String path) {
410
      if (!merged.hasPath(path)) {
×
411
        return false;
×
412
      } else if (customized.hasPathOrNull(path)) {
×
413
        return !customized.getIsNull(path);
×
414
      }
415
      return true;
×
416
    }
417
  }
418
}
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