diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 280a0d94b8..b92dd26bf4 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -334,6 +334,21 @@ + + + + + + + + + + + + + + + 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..021e2f196e 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().get( + 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/ConfigSource.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/ConfigSource.java new file mode 100644 index 0000000000..4918b5ed8e --- /dev/null +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/ConfigSource.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.benmanes.caffeine.jcache.configuration; + +import java.net.URI; + +import com.typesafe.config.Config; + +/** + * A provider for the external configuration. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@FunctionalInterface +public interface ConfigSource { + + /** + * Returns a {@link Config} that provides the cache configurations. + * + * @param uri a uri that may assist in resolve to the resource + * @param classloader the classloader to load with + * @return a configuration + */ + Config get(URI uri, ClassLoader classloader); +} 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..4538f33aec 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 @@ -19,8 +19,10 @@ 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; @@ -28,6 +30,7 @@ import java.util.Set; 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 +47,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 +60,8 @@ public final class TypesafeConfigurator { static final Logger logger = System.getLogger(TypesafeConfigurator.class.getName()); + static ConfigSource configSource = TypesafeConfigurator::resolveConfig; static FactoryCreator factoryCreator = FactoryBuilder::factoryOf; - static Supplier configSource = ConfigFactory::load; private TypesafeConfigurator() {} @@ -118,21 +123,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(ClassLoader)}. + * 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(ClassLoader)}. + * 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(ConfigSource configSource) { TypesafeConfigurator.configSource = requireNonNull(configSource); } /** Returns the strategy for loading the configuration. */ - public static Supplier configSource() { + public static ConfigSource 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 ((uri.getScheme() != null) && uri.getScheme().equalsIgnoreCase("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().equalsIgnoreCase("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..864e769e46 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,10 +15,13 @@ */ 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; @@ -29,24 +32,100 @@ 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 ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + final ConfigSource defaultConfigSource = configSource(); + + @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((ConfigSource) null)); + } + + @Test + public void configSource_null() { + assertThrows(NullPointerException.class, () -> configSource().get(null, null)); + assertThrows(NullPointerException.class, () -> configSource().get(null, classloader)); + assertThrows(NullPointerException.class, () -> configSource().get(URI.create(""), null)); + } + + @Test + public void configSource_load() { + assertThat(configSource().get(URI.create(getClass().getName()), classloader)) + .isSameInstanceAs(ConfigFactory.load()); + assertThat(configSource().get(URI.create("rmi:/abc"), classloader)) + .isSameInstanceAs(ConfigFactory.load()); + } + + @Test + public void configSource_classpath_present() { + var inferred = configSource().get(URI.create("custom.properties"), classloader); + assertThat(inferred.getInt("caffeine.jcache.classpath.policy.maximum.size")).isEqualTo(500); + + var explicit = configSource().get(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().get(URI.create("absent.conf"), classloader)); + assertThrows(ConfigException.IO.class, () -> + configSource().get(URI.create("classpath:absent.conf"), classloader)); + } + + @Test + public void configSource_classpath_invalid() { + assertThrows(ConfigException.Parse.class, () -> + configSource().get(URI.create("invalid.conf"), classloader)); + } + + @Test + public void configSource_file() throws URISyntaxException { + var config = configSource().get( + 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().get(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().get(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