From 35b740cfc4481d24d6f559ad8d3ff1adfc8add2a Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 4 Mar 2023 22:14:25 -0800 Subject: [PATCH] Allow jcache to load its configuration from a uri (fixes #877) --- config/spotbugs/exclude.xml | 10 +++ .../caffeine/jcache/CacheFactory.java | 20 +++-- .../caffeine/jcache/CacheManagerImpl.java | 9 +- .../configuration/TypesafeConfigurator.java | 64 ++++++++++++-- .../jcache/spi/CaffeineCachingProvider.java | 10 +-- .../TypesafeConfigurationTest.java | 88 +++++++++++++++++-- jcache/src/test/resources/custom.properties | 1 + jcache/src/test/resources/invalid.conf | 1 + 8 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 jcache/src/test/resources/custom.properties create mode 100644 jcache/src/test/resources/invalid.conf diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 280a0d94b8..19d16126dd 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -334,6 +334,16 @@ + + + + + + + + + + diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java index 2f22e1245c..5f8aaa9e74 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java @@ -56,23 +56,24 @@ private CacheFactory() {} /** * Returns if the cache definition is found in the external settings file. * + * @param cacheManager the owner * @param cacheName the name of the cache * @return {@code true} if a definition exists */ - public static boolean isDefinedExternally(String cacheName) { - return TypesafeConfigurator.cacheNames(rootConfig()).contains(cacheName); + public static boolean isDefinedExternally(CacheManager cacheManager, String cacheName) { + return TypesafeConfigurator.cacheNames(rootConfig(cacheManager)).contains(cacheName); } /** * Returns a newly created cache instance if a definition is found in the external settings file. * - * @param cacheManager the owner of the cache instance + * @param cacheManager the owner * @param cacheName the name of the cache * @return a new cache instance or null if the named cache is not defined in the settings file */ public static @Nullable CacheProxy tryToCreateFromExternalSettings( CacheManager cacheManager, String cacheName) { - return TypesafeConfigurator.from(rootConfig(), cacheName) + return TypesafeConfigurator.from(rootConfig(cacheManager), cacheName) .map(configuration -> createCache(cacheManager, cacheName, configuration)) .orElse(null); } @@ -87,24 +88,25 @@ public static boolean isDefinedExternally(String cacheName) { */ public static CacheProxy createCache(CacheManager cacheManager, String cacheName, Configuration configuration) { - CaffeineConfiguration config = resolveConfigurationFor(configuration); + CaffeineConfiguration config = resolveConfigurationFor(cacheManager, configuration); return new Builder<>(cacheManager, cacheName, config).build(); } /** Returns the resolved configuration. */ - private static Config rootConfig() { - return requireNonNull(TypesafeConfigurator.configSource().get()); + private static Config rootConfig(CacheManager cacheManager) { + return requireNonNull(TypesafeConfigurator.configSource().apply( + cacheManager.getURI(), cacheManager.getClassLoader())); } /** Copies the configuration and overlays it on top of the default settings. */ @SuppressWarnings("PMD.AccessorMethodGeneration") private static CaffeineConfiguration resolveConfigurationFor( - Configuration configuration) { + CacheManager cacheManager, Configuration configuration) { if (configuration instanceof CaffeineConfiguration) { return new CaffeineConfiguration<>((CaffeineConfiguration) configuration); } - CaffeineConfiguration template = TypesafeConfigurator.defaults(rootConfig()); + CaffeineConfiguration template = TypesafeConfigurator.defaults(rootConfig(cacheManager)); if (configuration instanceof CompleteConfiguration) { CompleteConfiguration complete = (CompleteConfiguration) configuration; template.setReadThrough(complete.isReadThrough()); diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java index 4bd10addc8..0ea06e66b9 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java @@ -35,8 +35,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; - /** * An implementation of JSR-107 {@link CacheManager} that manages Caffeine-based caches. * @@ -55,12 +53,11 @@ public final class CacheManagerImpl implements CacheManager { private volatile boolean closed; - public CacheManagerImpl(CachingProvider cacheProvider, + public CacheManagerImpl(CachingProvider cacheProvider, boolean runsAsAnOsgiBundle, URI uri, ClassLoader classLoader, Properties properties) { - this.runsAsAnOsgiBundle = (cacheProvider instanceof CaffeineCachingProvider) - && ((CaffeineCachingProvider) cacheProvider).isOsgiComponent(); this.classLoaderReference = new WeakReference<>(requireNonNull(classLoader)); this.cacheProvider = requireNonNull(cacheProvider); + this.runsAsAnOsgiBundle = runsAsAnOsgiBundle; this.properties = requireNonNull(properties); this.caches = new ConcurrentHashMap<>(); this.uri = requireNonNull(uri); @@ -102,7 +99,7 @@ public > Cache createCache( CacheProxy cache = caches.compute(cacheName, (name, existing) -> { if ((existing != null) && !existing.isClosed()) { throw new CacheException("Cache " + cacheName + " already exists"); - } else if (CacheFactory.isDefinedExternally(cacheName)) { + } else if (CacheFactory.isDefinedExternally(this, cacheName)) { throw new CacheException("Cache " + cacheName + " is configured externally"); } return CacheFactory.createCache(this, cacheName, configuration); diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java index a2c646ebee..51f276131d 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java @@ -15,19 +15,24 @@ */ package com.github.benmanes.caffeine.jcache.configuration; +import static java.util.Locale.US; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import java.io.File; import java.lang.System.Logger; import java.lang.System.Logger.Level; +import java.net.URI; import java.util.Collections; import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Supplier; +import javax.cache.CacheManager; import javax.cache.configuration.Factory; import javax.cache.configuration.FactoryBuilder; import javax.cache.configuration.MutableCacheEntryListenerConfiguration; @@ -44,6 +49,8 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigParseOptions; +import com.typesafe.config.ConfigSyntax; /** * Static utility methods pertaining to externalized {@link CaffeineConfiguration} entries using the @@ -55,8 +62,8 @@ public final class TypesafeConfigurator { static final Logger logger = System.getLogger(TypesafeConfigurator.class.getName()); + static BiFunction configSource = TypesafeConfigurator::resolveConfig; static FactoryCreator factoryCreator = FactoryBuilder::factoryOf; - static Supplier configSource = ConfigFactory::load; private TypesafeConfigurator() {} @@ -118,21 +125,68 @@ public static void setFactoryCreator(FactoryCreator factoryCreator) { } /** - * Specifies how the {@link Config} instance should be loaded. The default strategy uses - * {@link ConfigFactory#load()}. The configuration is retrieved on-demand, allowing for it to be - * reloaded, and it is assumed that the source caches it as needed. + * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri + * provided by {@link CacheManager#getURI()} as an optional override location to parse from a + * file system or classpath resource, or else returns {@link ConfigFactory#load()}. The + * configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed that + * the source caches it as needed. * * @param configSource the strategy for loading the configuration */ public static void setConfigSource(Supplier configSource) { + requireNonNull(configSource); + setConfigSource((uri, classloader) -> configSource.get()); + } + + /** + * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri + * provided by {@link CacheManager#getURI()} as an optional override location to parse from a + * file system or classpath resource, or else returns {@link ConfigFactory#load()}. The + * configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed that + * the source caches it as needed. + * + * @param configSource the strategy for loading the configuration from a uri + */ + public static void setConfigSource(BiFunction configSource) { TypesafeConfigurator.configSource = requireNonNull(configSource); } /** Returns the strategy for loading the configuration. */ - public static Supplier configSource() { + public static BiFunction configSource() { return TypesafeConfigurator.configSource; } + /** Returns the configuration by applying the default strategy. */ + private static Config resolveConfig(URI uri, ClassLoader classloader) { + requireNonNull(uri); + requireNonNull(classloader); + var options = ConfigParseOptions.defaults().setAllowMissing(false); + if (Objects.equals(uri.getScheme(), "file")) { + return ConfigFactory.parseFile(new File(uri), options); + } else if (isResource(uri)) { + return ConfigFactory.parseResources(uri.getSchemeSpecificPart(), options); + } + return ConfigFactory.load(classloader); + } + + /** Returns if the uri is a file or classpath resource. */ + private static boolean isResource(URI uri) { + if ((uri.getScheme() != null) && !uri.getScheme().toLowerCase(US).equals("classpath")) { + return false; + } + var path = uri.getSchemeSpecificPart(); + int dotIndex = path.lastIndexOf('.'); + if (dotIndex != -1) { + var extension = path.substring(dotIndex + 1); + for (var format : ConfigSyntax.values()) { + if (format.toString().equalsIgnoreCase(extension)) { + return true; + } + } + } + return false; + } + /** A one-shot builder for creating a configuration instance. */ private static final class Configurator { final CaffeineConfiguration configuration; diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/spi/CaffeineCachingProvider.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/spi/CaffeineCachingProvider.java index 307199bffc..5dc3a2672d 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/spi/CaffeineCachingProvider.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/spi/CaffeineCachingProvider.java @@ -39,9 +39,7 @@ import org.osgi.service.component.annotations.Component; import com.github.benmanes.caffeine.jcache.CacheManagerImpl; -import com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator; import com.google.errorprone.annotations.concurrent.GuardedBy; -import com.typesafe.config.ConfigFactory; /** * A provider that produces a JCache implementation backed by Caffeine. Typically, this provider is @@ -103,7 +101,8 @@ public CacheManager getCacheManager(URI uri, ClassLoader classLoader, Properties managerClassLoader, any -> new HashMap<>()); return cacheManagersByURI.computeIfAbsent(managerURI, any -> { Properties managerProperties = (properties == null) ? getDefaultProperties() : properties; - return new CacheManagerImpl(this, managerURI, managerClassLoader, managerProperties); + return new CacheManagerImpl(this, isOsgiComponent, + managerURI, managerClassLoader, managerProperties); }); } } @@ -267,10 +266,5 @@ public Enumeration getResources(String name) throws IOException { @SuppressWarnings("unused") private void activate() { isOsgiComponent = true; - TypesafeConfigurator.setConfigSource(() -> ConfigFactory.load(DEFAULT_CLASS_LOADER)); - } - - public boolean isOsgiComponent() { - return isOsgiComponent; } } diff --git a/jcache/src/test/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurationTest.java b/jcache/src/test/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurationTest.java index 0eaa3c7567..206b5c17fb 100644 --- a/jcache/src/test/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurationTest.java +++ b/jcache/src/test/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurationTest.java @@ -15,13 +15,17 @@ */ package com.github.benmanes.caffeine.jcache.configuration; +import static com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator.configSource; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Optional; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import java.util.function.Supplier; import javax.cache.Cache; @@ -29,24 +33,98 @@ import javax.cache.expiry.Duration; import javax.cache.expiry.ExpiryPolicy; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.github.benmanes.caffeine.jcache.copy.JavaSerializationCopier; import com.google.common.collect.Iterables; import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; /** * @author ben.manes@gmail.com (Ben Manes) */ public final class TypesafeConfigurationTest { + final BiFunction defaultConfigSource = configSource(); + final ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + + @BeforeMethod + public void before() { + TypesafeConfigurator.setConfigSource(defaultConfigSource); + } + + @Test + public void setConfigSource_supplier() { + TypesafeConfigurator.setConfigSource(() -> null); + assertThat(configSource()).isNotSameInstanceAs(defaultConfigSource); + + assertThrows(NullPointerException.class, () -> + TypesafeConfigurator.setConfigSource((Supplier) null)); + } + + @Test + public void setConfigSource_function() { + TypesafeConfigurator.setConfigSource((uri, classloader) -> null); + assertThat(configSource()).isNotSameInstanceAs(defaultConfigSource); + + assertThrows(NullPointerException.class, () -> + TypesafeConfigurator.setConfigSource((BiFunction) null)); + } + + @Test + public void configSource_null() { + assertThrows(NullPointerException.class, () -> configSource().apply(null, null)); + assertThrows(NullPointerException.class, () -> configSource().apply(null, classloader)); + assertThrows(NullPointerException.class, () -> configSource().apply(URI.create(""), null)); + } + + @Test + public void configSource_load() { + assertThat(configSource().apply(URI.create(getClass().getName()), classloader)) + .isSameInstanceAs(ConfigFactory.load()); + } + + @Test + public void configSource_classpath_present() { + var inferred = configSource().apply(URI.create("custom.properties"), classloader); + assertThat(inferred.getInt("caffeine.jcache.classpath.policy.maximum.size")).isEqualTo(500); + + var explicit = configSource().apply(URI.create("classpath:custom.properties"), classloader); + assertThat(explicit.getInt("caffeine.jcache.classpath.policy.maximum.size")).isEqualTo(500); + } + + @Test + public void configSource_classpath_absent() { + assertThrows(ConfigException.IO.class, () -> + configSource().apply(URI.create("absent.conf"), classloader)); + assertThrows(ConfigException.IO.class, () -> + configSource().apply(URI.create("classpath:absent.conf"), classloader)); + } + + @Test + public void configSource_classpath_invalid() { + assertThrows(ConfigException.Parse.class, () -> + configSource().apply(URI.create("invalid.conf"), classloader)); + } + + @Test + public void configSource_file() throws URISyntaxException { + var config = configSource().apply( + getClass().getResource("/custom.properties").toURI(), classloader); + assertThat(config.getInt("caffeine.jcache.classpath.policy.maximum.size")).isEqualTo(500); + } + + @Test + public void configSource_file_absent() { + assertThrows(ConfigException.IO.class, () -> + configSource().apply(URI.create("file:/absent.conf"), classloader)); + } @Test - public void configSource() { - Config config = ConfigFactory.load(); - Supplier configSource = () -> config; - TypesafeConfigurator.setConfigSource(configSource); - assertThat(TypesafeConfigurator.configSource()).isEqualTo(configSource); + public void configSource_file_invalid() { + assertThrows(ConfigException.IO.class, () -> + configSource().apply(URI.create("file:/invalid.conf"), classloader)); } @Test diff --git a/jcache/src/test/resources/custom.properties b/jcache/src/test/resources/custom.properties new file mode 100644 index 0000000000..b5abd04779 --- /dev/null +++ b/jcache/src/test/resources/custom.properties @@ -0,0 +1 @@ +caffeine.jcache.classpath.policy.maximum.size = 500 diff --git a/jcache/src/test/resources/invalid.conf b/jcache/src/test/resources/invalid.conf new file mode 100644 index 0000000000..66731a6443 --- /dev/null +++ b/jcache/src/test/resources/invalid.conf @@ -0,0 +1 @@ +This is not a hocon configuration file