diff --git a/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/CacheWrapper.java b/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/CacheWrapper.java new file mode 100644 index 0000000..83c8f3f --- /dev/null +++ b/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/CacheWrapper.java @@ -0,0 +1,9 @@ +package io.github.xanthic.cache.springjdk17; + +import org.springframework.cache.Cache; + +record CacheWrapper(Cache cache, boolean custom) { + CacheWrapper(Cache cache) { + this(cache, false); + } +} diff --git a/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/XanthicSpringCacheManager.java b/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/XanthicSpringCacheManager.java index 2071b8c..13a693b 100644 --- a/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/XanthicSpringCacheManager.java +++ b/spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/XanthicSpringCacheManager.java @@ -2,13 +2,15 @@ import io.github.xanthic.cache.core.CacheApi; import io.github.xanthic.cache.core.CacheApiSpec; -import lombok.Getter; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.lang.Nullable; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -21,9 +23,7 @@ */ public class XanthicSpringCacheManager implements CacheManager { - private final Map cacheMap = new ConcurrentHashMap<>(); - @Getter - private final Set customCacheNames = ConcurrentHashMap.newKeySet(); + private final Map cacheMap = new ConcurrentHashMap<>(); private final Consumer> spec; private final boolean dynamic; @@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer> spec, @N if (cacheNames != null) { this.dynamic = false; for (String name : cacheNames) { - this.cacheMap.put(name, createCache(name, this.spec)); + this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec))); } } else { this.dynamic = true; @@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer> spec, @N @Nullable public Cache getCache(@NotNull String name) { // Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066 - Cache optimistic = cacheMap.get(name); - if (optimistic != null || !dynamic) - return optimistic; + CacheWrapper optimistic = cacheMap.get(name); + if (optimistic != null) + return optimistic.cache(); + if (!dynamic) + return null; - return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec)); + return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache(); } @Override public @NotNull Collection getCacheNames() { - return this.cacheMap.keySet(); + return Collections.unmodifiableSet(cacheMap.keySet()); + } + + @NotNull + @Deprecated + @VisibleForTesting + public Set getCustomCacheNames() { + // unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice + Set names = new HashSet<>(); + cacheMap.forEach((k, v) -> { + if (v.custom()) { + names.add(k); + } + }); + return Collections.unmodifiableSet(names); } /** @@ -81,8 +97,18 @@ public Cache getCache(@NotNull String name) { public void registerCache(String name, Consumer> spec) { if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow creation of new caches."); - this.cacheMap.put(name, createCache(name, spec)); - this.customCacheNames.add(name); + this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true)); + } + + /** + * Removes a named cache from this cache manager. + * + * @param name the name of the cache + */ + public void removeCache(String name) { + if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches."); + + this.cacheMap.remove(name); } private Cache createCache(String name, Consumer> spec) { diff --git a/spring-java17/src/test/java/io/github/xanthic/cache/springjdk17/SpringCacheTest.java b/spring-java17/src/test/java/io/github/xanthic/cache/springjdk17/SpringCacheTest.java index e046c12..3711fff 100644 --- a/spring-java17/src/test/java/io/github/xanthic/cache/springjdk17/SpringCacheTest.java +++ b/spring-java17/src/test/java/io/github/xanthic/cache/springjdk17/SpringCacheTest.java @@ -63,18 +63,42 @@ public void putGetClearTest() { @Test @DisplayName("Tests the registration and usage of a custom cache") + @SuppressWarnings("deprecation") public void registerCustomCacheTest() { XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager; - xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> { + String name = "my-custom-cache"; + xanthicSpringCacheManager.registerCache(name, spec -> { spec.maxSize(1L); }); // registration check - Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains("my-custom-cache"), "getCustomCacheNames should contain my-custom-cache"); + Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains(name), "getCustomCacheNames should contain " + name); // cache available - Cache cache = cacheManager.getCache("my-custom-cache"); - Assertions.assertNotNull(cache, "my-custom-cache should not be null"); + Cache cache = cacheManager.getCache(name); + Assertions.assertNotNull(cache, name + " should not be null"); + + // remove cache + xanthicSpringCacheManager.removeCache(name); + Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present"); + } + + @Test + @DisplayName("Tests that dynamic caches can be removed from the manager") + @SuppressWarnings("deprecation") + public void removeDynamicTest() { + XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager; + + // create dynamic cache + String name = "my-dynamic-cache"; + Cache cache = xanthicSpringCacheManager.getCache(name); + Assertions.assertNotNull(cache, name + " should not be null"); + Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager"); + Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache"); + + // remove cache + xanthicSpringCacheManager.removeCache(name); + Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager"); } @Test diff --git a/spring/src/main/java/io/github/xanthic/cache/spring/CacheWrapper.java b/spring/src/main/java/io/github/xanthic/cache/spring/CacheWrapper.java new file mode 100644 index 0000000..fbe2641 --- /dev/null +++ b/spring/src/main/java/io/github/xanthic/cache/spring/CacheWrapper.java @@ -0,0 +1,18 @@ +package io.github.xanthic.cache.spring; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.springframework.cache.Cache; + +@Value +@Accessors(fluent = true) +@RequiredArgsConstructor +class CacheWrapper { + Cache cache; + boolean custom; + + CacheWrapper(Cache cache) { + this(cache, false); + } +} diff --git a/spring/src/main/java/io/github/xanthic/cache/spring/XanthicSpringCacheManager.java b/spring/src/main/java/io/github/xanthic/cache/spring/XanthicSpringCacheManager.java index 6d2eda8..3f8cae0 100644 --- a/spring/src/main/java/io/github/xanthic/cache/spring/XanthicSpringCacheManager.java +++ b/spring/src/main/java/io/github/xanthic/cache/spring/XanthicSpringCacheManager.java @@ -2,13 +2,15 @@ import io.github.xanthic.cache.core.CacheApi; import io.github.xanthic.cache.core.CacheApiSpec; -import lombok.Getter; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.lang.Nullable; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -21,9 +23,7 @@ */ public class XanthicSpringCacheManager implements CacheManager { - private final Map cacheMap = new ConcurrentHashMap<>(); - @Getter - private final Set customCacheNames = ConcurrentHashMap.newKeySet(); + private final Map cacheMap = new ConcurrentHashMap<>(); private final Consumer> spec; private final boolean dynamic; @@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer> spec, @N if (cacheNames != null) { this.dynamic = false; for (String name : cacheNames) { - this.cacheMap.put(name, createCache(name, this.spec)); + this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec))); } } else { this.dynamic = true; @@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer> spec, @N @Nullable public Cache getCache(@NotNull String name) { // Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066 - Cache optimistic = cacheMap.get(name); - if (optimistic != null || !dynamic) - return optimistic; + CacheWrapper optimistic = cacheMap.get(name); + if (optimistic != null) + return optimistic.cache(); + if (!dynamic) + return null; - return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec)); + return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache(); } @Override public @NotNull Collection getCacheNames() { - return this.cacheMap.keySet(); + return Collections.unmodifiableSet(cacheMap.keySet()); + } + + @NotNull + @Deprecated + @VisibleForTesting + public Set getCustomCacheNames() { + // unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice + Set names = new HashSet<>(); + cacheMap.forEach((k, v) -> { + if (v.custom()) { + names.add(k); + } + }); + return Collections.unmodifiableSet(names); } /** @@ -81,8 +97,18 @@ public Cache getCache(@NotNull String name) { public void registerCache(String name, Consumer> spec) { if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow creation of new caches."); - this.cacheMap.put(name, createCache(name, spec)); - this.customCacheNames.add(name); + this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true)); + } + + /** + * Removes a named cache from this cache manager. + * + * @param name the name of the cache + */ + public void removeCache(String name) { + if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches."); + + this.cacheMap.remove(name); } private Cache createCache(String name, Consumer> spec) { diff --git a/spring/src/test/java/io/github/xanthic/cache/spring/SpringCacheTest.java b/spring/src/test/java/io/github/xanthic/cache/spring/SpringCacheTest.java index 9d458d0..93446b3 100644 --- a/spring/src/test/java/io/github/xanthic/cache/spring/SpringCacheTest.java +++ b/spring/src/test/java/io/github/xanthic/cache/spring/SpringCacheTest.java @@ -50,18 +50,42 @@ public void putGetClearTest() { @Test @DisplayName("Tests the registration and usage of a custom cache") + @SuppressWarnings("deprecation") public void registerCustomCacheTest() { XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager; - xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> { + String name = "my-custom-cache"; + xanthicSpringCacheManager.registerCache(name, spec -> { spec.maxSize(1L); }); // registration check - Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains("my-custom-cache"), "getCustomCacheNames should contain my-custom-cache"); + Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains(name), "getCustomCacheNames should contain " + name); // cache available - Cache cache = cacheManager.getCache("my-custom-cache"); - Assertions.assertNotNull(cache, "my-custom-cache should not be null"); + Cache cache = cacheManager.getCache(name); + Assertions.assertNotNull(cache, name + " should not be null"); + + // remove cache + xanthicSpringCacheManager.removeCache(name); + Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present"); + } + + @Test + @DisplayName("Tests that dynamic caches can be removed from the manager") + @SuppressWarnings("deprecation") + public void removeDynamicTest() { + XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager; + + // create dynamic cache + String name = "my-dynamic-cache"; + Cache cache = xanthicSpringCacheManager.getCache(name); + Assertions.assertNotNull(cache, name + " should not be null"); + Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager"); + Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache"); + + // remove cache + xanthicSpringCacheManager.removeCache(name); + Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager"); } @Test