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

ben-manes / caffeine / #4911

22 Jun 2025 04:59AM UTC coverage: 99.987% (-0.01%) from 100.0%
#4911

push

github

ben-manes
increase branch coverage

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

1 existing line in 1 file now uncovered.

7734 of 7735 relevant lines covered (99.99%)

1.0 hits per line

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

99.38
/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
      return config.hasPath("caffeine.jcache." + cacheName)
1✔
111
          ? Optional.of(new Configurator<K, V>(config, cacheName).configure())
1✔
112
          : Optional.empty();
1✔
113
    } catch (ConfigException.BadPath e) {
1✔
114
      logger.log(Level.WARNING, "Failed to load cache configuration", e);
1✔
115
      return Optional.empty();
1✔
116
    }
117
  }
118

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

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

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

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

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

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

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

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

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

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

242
      return configuration;
1✔
243
    }
244

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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