Skip to content

Commit

Permalink
Allow jcache to load its configuration from a uri (fixes #877)
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-manes committed Mar 5, 2023
1 parent 03e9926 commit 90b5a00
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 34 deletions.
10 changes: 10 additions & 0 deletions config/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,16 @@
<Method name="deserialize"/>
<Bug pattern="OBJECT_DESERIALIZATION"/>
</Match>
<Match>
<Class name="com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator"/>
<Method name="resolveConfig"/>
<Bug pattern="PATH_TRAVERSAL_IN"/>
</Match>
<Match>
<Class name="com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator"/>
<Method name="isResource"/>
<Bug pattern="IMPROPER_UNICODE"/>
</Match>
<Match>
<Class name="com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator$Configurator"/>
<Method name="addLazyExpiration"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,29 @@
* @author [email protected] (Ben Manes)
*/
final class CacheFactory {

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 <K, V> CacheProxy<K, V> tryToCreateFromExternalSettings(
CacheManager cacheManager, String cacheName) {
return TypesafeConfigurator.<K, V>from(rootConfig(), cacheName)
return TypesafeConfigurator.<K, V>from(rootConfig(cacheManager), cacheName)
.map(configuration -> createCache(cacheManager, cacheName, configuration))
.orElse(null);
}
Expand All @@ -87,24 +87,25 @@ public static boolean isDefinedExternally(String cacheName) {
*/
public static <K, V> CacheProxy<K, V> createCache(CacheManager cacheManager,
String cacheName, Configuration<K, V> configuration) {
CaffeineConfiguration<K, V> config = resolveConfigurationFor(configuration);
CaffeineConfiguration<K, V> 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 <K, V> CaffeineConfiguration<K, V> resolveConfigurationFor(
Configuration<K, V> configuration) {
CacheManager cacheManager, Configuration<K, V> configuration) {
if (configuration instanceof CaffeineConfiguration<?, ?>) {
return new CaffeineConfiguration<>((CaffeineConfiguration<K, V>) configuration);
}

CaffeineConfiguration<K, V> template = TypesafeConfigurator.defaults(rootConfig());
CaffeineConfiguration<K, V> template = TypesafeConfigurator.defaults(rootConfig(cacheManager));
if (configuration instanceof CompleteConfiguration<?, ?>) {
CompleteConfiguration<K, V> complete = (CompleteConfiguration<K, V>) configuration;
template.setReadThrough(complete.isReadThrough());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
Expand Down Expand Up @@ -102,7 +99,7 @@ public <K, V, C extends Configuration<K, V>> Cache<K, V> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -55,8 +62,8 @@
public final class TypesafeConfigurator {
static final Logger logger = System.getLogger(TypesafeConfigurator.class.getName());

static BiFunction<URI, ClassLoader, Config> configSource = TypesafeConfigurator::resolveConfig;
static FactoryCreator factoryCreator = FactoryBuilder::factoryOf;
static Supplier<Config> configSource = ConfigFactory::load;

private TypesafeConfigurator() {}

Expand Down Expand Up @@ -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<Config> 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<URI, ClassLoader, Config> configSource) {
TypesafeConfigurator.configSource = requireNonNull(configSource);
}

/** Returns the strategy for loading the configuration. */
public static Supplier<Config> configSource() {
public static BiFunction<URI, ClassLoader, Config> 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<K, V> {
final CaffeineConfiguration<K, V> configuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
});
}
}
Expand Down Expand Up @@ -267,10 +266,5 @@ public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unused")
private void activate() {
isOsgiComponent = true;
TypesafeConfigurator.setConfigSource(() -> ConfigFactory.load(DEFAULT_CLASS_LOADER));
}

public boolean isOsgiComponent() {
return isOsgiComponent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,116 @@
*/
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;
import javax.cache.Caching;
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 [email protected] (Ben Manes)
*/
public final class TypesafeConfigurationTest {
final BiFunction<URI, ClassLoader, Config> 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<Config>) null));
}

@Test
public void setConfigSource_function() {
TypesafeConfigurator.setConfigSource((uri, classloader) -> null);
assertThat(configSource()).isNotSameInstanceAs(defaultConfigSource);

assertThrows(NullPointerException.class, () ->
TypesafeConfigurator.setConfigSource((BiFunction<URI, ClassLoader, Config>) 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<Config> 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
Expand Down
1 change: 1 addition & 0 deletions jcache/src/test/resources/custom.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
caffeine.jcache.classpath.policy.maximum.size = 500
1 change: 1 addition & 0 deletions jcache/src/test/resources/invalid.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is not a hocon configuration file

0 comments on commit 90b5a00

Please sign in to comment.