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

ben-manes / caffeine / #4942

08 Jul 2025 05:37AM UTC coverage: 99.962% (-0.04%) from 100.0%
#4942

push

github

ben-manes
increase branch coverage

3789 of 3804 branches covered (99.61%)

18 of 18 new or added lines in 2 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

7812 of 7815 relevant lines covered (99.96%)

1.0 hits per line

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

98.15
/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.Objects;
29
import java.util.Optional;
30
import java.util.OptionalLong;
31
import java.util.Set;
32
import java.util.concurrent.atomic.AtomicReference;
33
import java.util.function.Supplier;
34

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

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

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

56
import jakarta.inject.Inject;
57

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

68
  static final AtomicReference<ConfigSource> configSource =
1✔
69
      new AtomicReference<>(TypesafeConfigurator::resolveConfig);
70
  static final AtomicReference<FactoryCreator> factoryCreator =
1✔
71
      new AtomicReference<>(FactoryBuilder::factoryOf);
72

73
  private TypesafeConfigurator() {}
74

75
  /**
76
   * Retrieves the names of the caches defined in the configuration resource.
77
   *
78
   * @param config the configuration resource
79
   * @return the names of the configured caches
80
   */
81
  public static Set<String> cacheNames(Config config) {
82
    return config.hasPath("caffeine.jcache")
1✔
83
        ? Collections.unmodifiableSet(config.getObject("caffeine.jcache").keySet())
1✔
84
        : Collections.emptySet();
1✔
85
  }
86

87
  /**
88
   * Retrieves the default cache settings from the configuration resource.
89
   *
90
   * @param config the configuration resource
91
   * @param <K> the type of keys maintained the cache
92
   * @param <V> the type of cached values
93
   * @return the default configuration for a cache
94
   */
95
  public static <K, V> CaffeineConfiguration<K, V> defaults(Config config) {
96
    return new Configurator<K, V>(config, "default").configure();
1✔
97
  }
98

99
  /**
100
   * Retrieves the cache's settings from the configuration resource if defined.
101
   *
102
   * @param config the configuration resource
103
   * @param cacheName the name of the cache
104
   * @param <K> the type of keys maintained the cache
105
   * @param <V> the type of cached values
106
   * @return the configuration for the cache
107
   */
108
  public static <K, V> Optional<CaffeineConfiguration<K, V>> from(Config config, String cacheName) {
109
    try {
110
      requireNonNull(cacheName);
1✔
111
      return config.hasPath("caffeine.jcache." + cacheName)
1✔
112
          ? Optional.of(new Configurator<K, V>(config, cacheName).configure())
1✔
113
          : Optional.empty();
1✔
114
    } catch (ConfigException.BadPath e) {
1✔
115
      logger.log(Level.WARNING, "Failed to load cache configuration", e);
1✔
116
      return Optional.empty();
1✔
117
    }
118
  }
119

120
  /**
121
   * Specifies how {@link Factory} instances are created for a given class name. The default
122
   * strategy uses {@link Class#newInstance()} and requires the class has a no-args constructor.
123
   *
124
   * @param factoryCreator the strategy for creating a factory
125
   */
126
  @Inject
127
  @SuppressWarnings({"deprecation", "UnnecessarilyVisible"})
128
  public static void setFactoryCreator(FactoryCreator factoryCreator) {
129
    TypesafeConfigurator.factoryCreator.set(requireNonNull(factoryCreator));
1✔
130
  }
1✔
131

132
  /** Returns the strategy for how factory instances are created. */
133
  public static FactoryCreator factoryCreator() {
134
    return requireNonNull(factoryCreator.get());
1✔
135
  }
136

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

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

164
  /** Returns the strategy for loading the configuration. */
165
  public static ConfigSource configSource() {
166
    return requireNonNull(configSource.get());
1✔
167
  }
168

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

196
  /** Returns if the uri is a file or classpath resource. */
197
  private static boolean isResource(URI uri) {
198
    if ((uri.getScheme() != null) && !uri.getScheme().equalsIgnoreCase("classpath")) {
1✔
199
      return false;
1✔
200
    }
201
    var path = uri.getSchemeSpecificPart();
1✔
202
    int dotIndex = path.lastIndexOf('.');
1✔
203
    if (dotIndex != -1) {
1✔
204
      var extension = path.substring(dotIndex + 1);
1✔
205
      for (var format : ConfigSyntax.values()) {
1✔
206
        if (format.toString().equalsIgnoreCase(extension)) {
1✔
207
          return true;
1✔
208
        }
209
      }
210
    }
211
    return false;
1✔
212
  }
213

214
  /** A one-shot builder for creating a configuration instance. */
215
  static final class Configurator<K, V> {
216
    final CaffeineConfiguration<K, V> configuration;
217
    final Config customized;
218
    final Config merged;
219
    final Config root;
220

221
    Configurator(Config config, String cacheName) {
1✔
222
      this.root = requireNonNull(config);
1✔
223
      this.configuration = new CaffeineConfiguration<>();
1✔
224
      this.customized = root.getConfig("caffeine.jcache." + requireNonNull(cacheName));
1✔
225
      this.merged = customized.withFallback(root.getConfig("caffeine.jcache.default"));
1✔
226
    }
1✔
227

228
    /** Returns a configuration built from the external settings. */
229
    CaffeineConfiguration<K, V> configure() {
230
      addKeyValueTypes();
1✔
231
      addStoreByValue();
1✔
232
      addExecutor();
1✔
233
      addScheduler();
1✔
234
      addListeners();
1✔
235
      addReadThrough();
1✔
236
      addWriteThrough();
1✔
237
      addMonitoring();
1✔
238
      addLazyExpiration();
1✔
239
      addEagerExpiration();
1✔
240
      addRefresh();
1✔
241
      addMaximum();
1✔
242

243
      return configuration;
1✔
244
    }
245

246
    /** Adds the key and value class types. */
247
    private void addKeyValueTypes() {
248
      try {
249
        @SuppressWarnings("unchecked")
250
        var keyType = (Class<K>) Class.forName(merged.getString("key-type"));
1✔
251
        @SuppressWarnings("unchecked")
252
        var valueType = (Class<V>) Class.forName(merged.getString("value-type"));
1✔
253
        configuration.setTypes(keyType, valueType);
1✔
254
      } catch (ClassNotFoundException e) {
1✔
255
        throw new IllegalStateException(e);
1✔
256
      }
1✔
257
    }
1✔
258

259
    /** Adds the store-by-value settings. */
260
    private void addStoreByValue() {
261
      configuration.setStoreByValue(merged.getBoolean("store-by-value.enabled"));
1✔
262
      if (isSet("store-by-value.strategy")) {
1✔
263
        configuration.setCopierFactory(factoryCreator().factoryOf(
1✔
264
            merged.getString("store-by-value.strategy")));
1✔
265
      }
266
    }
1✔
267

268
    /** Adds the executor settings. */
269
    public void addExecutor() {
270
      if (isSet("executor")) {
1✔
271
        configuration.setExecutorFactory(factoryCreator()
1✔
272
            .factoryOf(merged.getString("executor")));
1✔
273
      }
274
    }
1✔
275

276
    /** Adds the scheduler settings. */
277
    public void addScheduler() {
278
      if (isSet("scheduler")) {
1✔
279
        configuration.setSchedulerFactory(factoryCreator()
1✔
280
            .factoryOf(merged.getString("scheduler")));
1✔
281
      }
282
    }
1✔
283

284
    /** Adds the entry listeners settings. */
285
    private void addListeners() {
286
      for (String path : merged.getStringList("listeners")) {
1✔
287
        Config listener = root.getConfig(path);
1✔
288

289
        Factory<? extends CacheEntryListener<? super K, ? super V>> listenerFactory =
290
            factoryCreator().factoryOf(listener.getString("class"));
1✔
291
        @Var Factory<? extends CacheEntryEventFilter<? super K, ? super V>> filterFactory = null;
1✔
292
        if (listener.hasPath("filter")) {
1✔
293
          filterFactory = factoryCreator().factoryOf(listener.getString("filter"));
1✔
294
        }
295
        boolean oldValueRequired = listener.getBoolean("old-value-required");
1✔
296
        boolean synchronous = listener.getBoolean("synchronous");
1✔
297
        configuration.addCacheEntryListenerConfiguration(
1✔
298
            new MutableCacheEntryListenerConfiguration<>(
299
                listenerFactory, filterFactory, oldValueRequired, synchronous));
300
      }
1✔
301
    }
1✔
302

303
    /** Adds the read through settings. */
304
    private void addReadThrough() {
305
      configuration.setReadThrough(merged.getBoolean("read-through.enabled"));
1✔
306
      if (isSet("read-through.loader")) {
1✔
307
        configuration.setCacheLoaderFactory(factoryCreator().factoryOf(
1✔
308
            merged.getString("read-through.loader")));
1✔
309
      }
310
    }
1✔
311

312
    /** Adds the write-through settings. */
313
    private void addWriteThrough() {
314
      configuration.setWriteThrough(merged.getBoolean("write-through.enabled"));
1✔
315
      if (isSet("write-through.writer")) {
1✔
316
        configuration.setCacheWriterFactory(factoryCreator().factoryOf(
1✔
317
            merged.getString("write-through.writer")));
1✔
318
      }
319
    }
1✔
320

321
    /** Adds the monitoring settings. */
322
    private void addMonitoring() {
323
      configuration.setNativeStatisticsEnabled(merged.getBoolean("monitoring.native-statistics"));
1✔
324
      configuration.setStatisticsEnabled(merged.getBoolean("monitoring.statistics"));
1✔
325
      configuration.setManagementEnabled(merged.getBoolean("monitoring.management"));
1✔
326
    }
1✔
327

328
    /** Adds the JCache specification's lazy expiration settings. */
329
    public void addLazyExpiration() {
330
      Duration creation = getDurationFor("policy.lazy-expiration.creation");
1✔
331
      Duration update = getDurationFor("policy.lazy-expiration.update");
1✔
332
      Duration access = getDurationFor("policy.lazy-expiration.access");
1✔
333
      requireNonNull(creation, "policy.lazy-expiration.creation may not be null");
1✔
334

335
      boolean eternal = Objects.equals(creation, Duration.ETERNAL)
1✔
336
          && Objects.equals(update, Duration.ETERNAL)
1✔
337
          && Objects.equals(access, Duration.ETERNAL);
1✔
338
      Factory<? extends ExpiryPolicy> factory = eternal
1✔
339
          ? EternalExpiryPolicy.factoryOf()
1✔
340
          : FactoryBuilder.factoryOf(new JCacheExpiryPolicy(creation, update, access));
1✔
341
      configuration.setExpiryPolicyFactory(factory);
1✔
342
    }
1✔
343

344
    /** Returns the duration for the expiration time. */
345
    private @Nullable Duration getDurationFor(String path) {
346
      if (!isSet(path)) {
1✔
347
        return null;
1✔
348
      }
349
      if (merged.getString(path).equalsIgnoreCase("eternal")) {
1✔
350
        return Duration.ETERNAL;
1✔
351
      }
352
      long millis = merged.getDuration(path, MILLISECONDS);
1✔
353
      return new Duration(MILLISECONDS, millis);
1✔
354
    }
355

356
    /** Adds the Caffeine eager expiration settings. */
357
    public void addEagerExpiration() {
358
      if (isSet("policy.eager-expiration.after-write")) {
1✔
359
        long nanos = merged.getDuration("policy.eager-expiration.after-write", NANOSECONDS);
1✔
360
        configuration.setExpireAfterWrite(OptionalLong.of(nanos));
1✔
361
      }
362
      if (isSet("policy.eager-expiration.after-access")) {
1✔
363
        long nanos = merged.getDuration("policy.eager-expiration.after-access", NANOSECONDS);
1✔
364
        configuration.setExpireAfterAccess(OptionalLong.of(nanos));
1✔
365
      }
366
      if (isSet("policy.eager-expiration.variable")) {
1✔
367
        configuration.setExpiryFactory(Optional.of(FactoryBuilder.factoryOf(
1✔
368
            merged.getString("policy.eager-expiration.variable"))));
1✔
369
      }
370
    }
1✔
371

372
    /** Adds the Caffeine refresh settings. */
373
    public void addRefresh() {
374
      if (isSet("policy.refresh.after-write")) {
1✔
375
        long nanos = merged.getDuration("policy.refresh.after-write", NANOSECONDS);
1✔
376
        configuration.setRefreshAfterWrite(OptionalLong.of(nanos));
1✔
377
      }
378
    }
1✔
379

380
    /** Adds the maximum size and weight bounding settings. */
381
    private void addMaximum() {
382
      if (isSet("policy.maximum.size")) {
1✔
383
        configuration.setMaximumSize(OptionalLong.of(merged.getLong("policy.maximum.size")));
1✔
384
      }
385
      if (isSet("policy.maximum.weight")) {
1✔
386
        configuration.setMaximumWeight(OptionalLong.of(merged.getLong("policy.maximum.weight")));
1✔
387
      }
388
      if (isSet("policy.maximum.weigher")) {
1✔
389
        configuration.setWeigherFactory(Optional.of(
1✔
390
            FactoryBuilder.factoryOf(merged.getString("policy.maximum.weigher"))));
1✔
391
      }
392
    }
1✔
393

394
    /** Returns if the value is present (not unset by the cache configuration). */
395
    private boolean isSet(String path) {
396
      if (!merged.hasPath(path)) {
1✔
397
        return false;
1✔
398
      } else if (customized.hasPathOrNull(path)) {
1✔
399
        return !customized.getIsNull(path);
1✔
400
      }
401
      return true;
1✔
402
    }
403
  }
404
}
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