From cadf5d27635921fd4a037731811615e62f65fbbf Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Thu, 14 Dec 2023 21:16:05 -0800 Subject: [PATCH 1/3] feat: Proxy cache inherits from passthrough. If the key isnt in the cache fallback to the passthough and cache the result. --- .../gestalt/config/decoder/ProxyDecoder.java | 100 ++++++++++-------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java index ebb165b15..543bc23b0 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java @@ -152,7 +152,7 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCa case CACHE: default: { - proxyHandler = new ProxyCacheInvocationHandler(path, methodResults); + proxyHandler = new ProxyCacheInvocationHandler(path, tags, decoderContext, methodResults); break; } } @@ -161,46 +161,11 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCa return ValidateOf.validateOf(myProxy, errors); } - private static class ProxyCacheInvocationHandler implements InvocationHandler { - private final String path; - private final Map methodResults; - - - private ProxyCacheInvocationHandler(String path, Map methodResults) { - this.path = path; - this.methodResults = methodResults; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - boolean isDefault = method.isDefault(); - - Object result = methodResults.get(methodName); - - Class type = method.getReturnType(); - if (result != null) { - return result; - } else if (isDefault) { - return MethodHandles.lookup() - .findSpecial( - method.getDeclaringClass(), - methodName, - MethodType.methodType(type, new Class[0]), - method.getDeclaringClass()) - .bindTo(proxy) - .invokeWithArguments(args); - } else { - throw new GestaltException("Failed to get cached object from proxy config while calling method: " + methodName + - " with type: " + type + " in path: " + path + "."); - } - } - } private static class ProxyPassThroughInvocationHandler implements InvocationHandler { - private final String path; - private final Tags tags; - private final DecoderContext decoderContext; + protected final String path; + protected final Tags tags; + protected final DecoderContext decoderContext; private ProxyPassThroughInvocationHandler(String path, Tags tags, DecoderContext decoderContext) { @@ -211,6 +176,16 @@ private ProxyPassThroughInvocationHandler(String path, Tags tags, DecoderContext @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Class returnType = method.getReturnType(); + + Optional result = retrieveConfig(proxy, method, args); + return result.orElseThrow(() -> + new GestaltException("Failed to get pass through object from proxy config while calling method: " + methodName + + " with type: " + returnType + " in path: " + path + ".")); + } + + protected Optional retrieveConfig(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); boolean isDefault = method.isDefault(); @@ -229,10 +204,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl String nextPath = PathUtil.pathForKey(path, name); - var result = decoderContext.getGestalt().getConfigOptional(nextPath, TypeCapture.of(genericType), tags); + Optional result = Optional.empty(); + if(decoderContext.getGestalt() != null) { + result = decoderContext.getGestalt().getConfigOptional(nextPath, TypeCapture.of(genericType), tags); + } if (result != null && result.isPresent()) { - return result.get(); + return result; } else { // if we have no value, check the config annotation for a default. @@ -243,12 +221,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl decoderContext); if (defaultValidateOf.hasResults()) { - return defaultValidateOf.results(); + return Optional.of(defaultValidateOf.results()); } } if (isDefault) { - return MethodHandles.lookup() + var defaultResult = MethodHandles.lookup() .findSpecial( method.getDeclaringClass(), methodName, @@ -256,10 +234,42 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl method.getDeclaringClass()) .bindTo(proxy) .invokeWithArguments(args); + + return Optional.of(defaultResult); } } - throw new GestaltException("Failed to get pass through object from proxy config while calling method: " + methodName + - " with type: " + returnType + " in path: " + path + "."); + return Optional.empty(); + } + } + + + private static class ProxyCacheInvocationHandler extends ProxyPassThroughInvocationHandler implements InvocationHandler { + private final Map methodResults; + + + private ProxyCacheInvocationHandler(String path, Tags tags, DecoderContext decoderContext, Map methodResults) { + super(path, tags, decoderContext); + this.methodResults = methodResults; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Class returnType = method.getReturnType(); + + Object result = methodResults.get(methodName); + if (result != null) { + return result; + } else { + Optional resultOptional = retrieveConfig(proxy, method, args); + var gestaltResult = resultOptional.orElseThrow(() -> + new GestaltException("Failed to get cached object from proxy config while calling method: " + methodName + + " with type: " + returnType + " in path: " + path + ".")); + + methodResults.put(methodName, gestaltResult); + + return gestaltResult; + } } } } From bc9cdbf649f54ab8c851677f25ab14a6e2bd7b42 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Thu, 14 Dec 2023 21:59:30 -0800 Subject: [PATCH 2/3] feat: register the proxy value with gestalt as a CoreReloadListener to get notified when Gestalt reloads its configs. When Gestalt reloads its configs clear the cache, it will refill as its methods get called. --- .../gestalt/config/decoder/ProxyDecoder.java | 20 ++- .../reload/CoreReloadListenersContainer.java | 9 +- .../config/decoder/ProxyDecoderTest.java | 127 ++++++++++++++++++ 3 files changed, 150 insertions(+), 6 deletions(-) diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java index 543bc23b0..7dd4dfdb4 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java @@ -4,10 +4,12 @@ import org.github.gestalt.config.entity.GestaltConfig; import org.github.gestalt.config.entity.ValidationError; import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.loader.ConfigLoaderRegistry; import org.github.gestalt.config.node.ConfigNode; import org.github.gestalt.config.node.LeafNode; import org.github.gestalt.config.node.MapNode; import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.reload.CoreReloadListener; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.utils.PathUtil; import org.github.gestalt.config.utils.ValidateOf; @@ -28,9 +30,11 @@ * @author Colin Redmond (c) 2023. */ public final class ProxyDecoder implements Decoder { + // For the proxy decoder, if we should use a cached value or call gestalt for the most recent value. private ProxyDecoderMode proxyDecoderMode = ProxyDecoderMode.CACHE; + private static String getConfigName(String methodName, Type returnType) { String name = methodName; if (methodName.startsWith("get")) { @@ -153,6 +157,9 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCa case CACHE: default: { proxyHandler = new ProxyCacheInvocationHandler(path, tags, decoderContext, methodResults); + if(decoderContext.getGestalt() != null) { + decoderContext.getGestalt().registerListener((ProxyCacheInvocationHandler) proxyHandler); + } break; } } @@ -162,7 +169,7 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCa } - private static class ProxyPassThroughInvocationHandler implements InvocationHandler { + static class ProxyPassThroughInvocationHandler implements InvocationHandler { protected final String path; protected final Tags tags; protected final DecoderContext decoderContext; @@ -243,7 +250,10 @@ protected Optional retrieveConfig(Object proxy, Method method, Object[] } - private static class ProxyCacheInvocationHandler extends ProxyPassThroughInvocationHandler implements InvocationHandler { + static class ProxyCacheInvocationHandler extends ProxyPassThroughInvocationHandler + implements InvocationHandler, CoreReloadListener { + + private static final System.Logger logger = System.getLogger(ProxyCacheInvocationHandler.class.getName()); private final Map methodResults; @@ -271,6 +281,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return gestaltResult; } } + + @Override + public void reload() { + logger.log(System.Logger.Level.DEBUG, "Reloading received on Proxy Cache Listener. Clearing Cache"); + methodResults.clear(); + } } } diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java b/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java index 68aec2979..148a841ac 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.WeakHashMap; /** * Store all core reload listeners and functionality to call the on reload. @@ -12,7 +13,7 @@ public class CoreReloadListenersContainer { /** * Listeners for the core reload. */ - protected final List listeners = new ArrayList<>(); + protected final WeakHashMap listeners = new WeakHashMap<>(); /** * Default constructor for CoreReloadStrategy. @@ -26,7 +27,7 @@ public CoreReloadListenersContainer() { * @param listener to register */ public void registerListener(CoreReloadListener listener) { - listeners.add(listener); + listeners.put(listener.hashCode(), listener); } /** @@ -35,13 +36,13 @@ public void registerListener(CoreReloadListener listener) { * @param listener to remove */ public void removeListener(CoreReloadListener listener) { - listeners.remove(listener); + listeners.remove(listener.hashCode()); } /** * called when the core has reloaded. */ public void reload() { - listeners.forEach(CoreReloadListener::reload); + listeners.forEach((k, v) -> v.reload()); } } diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java index 871550355..6f37e47ac 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java @@ -1,5 +1,7 @@ package org.github.gestalt.config.decoder; +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; import org.github.gestalt.config.entity.ValidationLevel; import org.github.gestalt.config.exceptions.GestaltConfigurationException; import org.github.gestalt.config.exceptions.GestaltException; @@ -8,6 +10,9 @@ import org.github.gestalt.config.node.*; import org.github.gestalt.config.path.mapper.StandardPathMapper; import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.reload.ManualConfigReloadStrategy; +import org.github.gestalt.config.source.MapConfigSource; +import org.github.gestalt.config.source.MapConfigSourceBuilder; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.test.classes.*; import org.github.gestalt.config.utils.ValidateOf; @@ -520,5 +525,127 @@ void decodeAnnotationsOnlyDefault() { Assertions.assertEquals("pass", results.getPassword()); Assertions.assertEquals("mysql.com", results.getUri()); } + + @Test + void decodeReload() throws GestaltException { + + // Create a map of configurations we wish to inject. + Map configs = new HashMap<>(); + configs.put("db.port", "100"); + configs.put("db.uri", "mysql.com"); + configs.put("db.password", "pass"); + + ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy(); + + // using the builder to layer on the configuration files. + // The later ones layer on and over write any values in the previous + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder() + .setCustomConfig(configs) + .addConfigReloadStrategy(reload) + .build()) + .setTreatNullValuesInClassAsErrors(false) + .setProxyDecoderMode(ProxyDecoderMode.CACHE) + .build(); + + gestalt.loadConfigs(); + + + DBInfoInterface results = gestalt.getConfig("db", DBInfoInterface.class); + + Assertions.assertEquals(100, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("mysql.com", results.getUri()); + + configs.put("db.port", "200"); + reload.reload(); + + Assertions.assertEquals(200, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("mysql.com", results.getUri()); + } + + @Test + void decodeReloadDefault() throws GestaltException { + + // Create a map of configurations we wish to inject. + Map configs = new HashMap<>(); + configs.put("db.port", "100"); + configs.put("db.uri", "mysql.com"); + configs.put("db.password", "pass"); + + ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy(); + + // using the builder to layer on the configuration files. + // The later ones layer on and over write any values in the previous + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder() + .setCustomConfig(configs) + .addConfigReloadStrategy(reload) + .build()) + .setTreatNullValuesInClassAsErrors(false) + .setProxyDecoderMode(ProxyDecoderMode.CACHE) + .build(); + + gestalt.loadConfigs(); + + + DBInfoInterface results = gestalt.getConfig("db", DBInfoInterface.class); + + Assertions.assertEquals(100, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("mysql.com", results.getUri()); + + configs.remove("db.port"); + configs.put("db.uri", "postgresql.org"); + reload.reload(); + + Assertions.assertEquals(10, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("postgresql.org", results.getUri()); + } + + @Test + void decodeReloadDAnnotationDefault() throws GestaltException { + + // Create a map of configurations we wish to inject. + Map configs = new HashMap<>(); + configs.put("db.channel", "100"); + configs.put("db.uri", "mysql.com"); + configs.put("db.password", "pass"); + + ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy(); + + // using the builder to layer on the configuration files. + // The later ones layer on and over write any values in the previous + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder() + .setCustomConfig(configs) + .addConfigReloadStrategy(reload) + .build()) + .setTreatNullValuesInClassAsErrors(false) + .setProxyDecoderMode(ProxyDecoderMode.CACHE) + .build(); + + gestalt.loadConfigs(); + + + IDBInfoAnnotations results = gestalt.getConfig("db", IDBInfoAnnotations.class); + + Assertions.assertEquals(100, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("mysql.com", results.getUri()); + + configs.remove("db.channel"); + configs.put("db.uri", "postgresql.org"); + reload.reload(); + + Assertions.assertEquals(1234, results.getPort()); + Assertions.assertEquals("pass", results.getPassword()); + Assertions.assertEquals("postgresql.org", results.getUri()); + } } From a7bdd1232607616bf8a8234d9cea6019999c0933 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Thu, 14 Dec 2023 22:13:31 -0800 Subject: [PATCH 3/3] fix: PMD issues. --- .../org/github/gestalt/config/decoder/ProxyDecoder.java | 7 +++---- .../config/reload/CoreReloadListenersContainer.java | 2 -- .../github/gestalt/config/decoder/ProxyDecoderTest.java | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java index 7dd4dfdb4..fbaf8d2cd 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ProxyDecoder.java @@ -4,7 +4,6 @@ import org.github.gestalt.config.entity.GestaltConfig; import org.github.gestalt.config.entity.ValidationError; import org.github.gestalt.config.exceptions.GestaltException; -import org.github.gestalt.config.loader.ConfigLoaderRegistry; import org.github.gestalt.config.node.ConfigNode; import org.github.gestalt.config.node.LeafNode; import org.github.gestalt.config.node.MapNode; @@ -35,7 +34,7 @@ public final class ProxyDecoder implements Decoder { private ProxyDecoderMode proxyDecoderMode = ProxyDecoderMode.CACHE; - private static String getConfigName(String methodName, Type returnType) { + private static String getConfigNameFromMethod(String methodName, Type returnType) { String name = methodName; if (methodName.startsWith("get")) { name = methodName.substring(3); @@ -108,7 +107,7 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCa if (configAnnotation != null && configAnnotation.path() != null && !configAnnotation.path().isEmpty()) { name = configAnnotation.path(); } else { - name = getConfigName(methodName, returnType); + name = getConfigNameFromMethod(methodName, returnType); } String nextPath = PathUtil.pathForKey(path, name); @@ -206,7 +205,7 @@ protected Optional retrieveConfig(Object proxy, Method method, Object[] if (configAnnotation != null && configAnnotation.path() != null && !configAnnotation.path().isEmpty()) { name = configAnnotation.path(); } else { - name = getConfigName(methodName, returnType); + name = getConfigNameFromMethod(methodName, returnType); } String nextPath = PathUtil.pathForKey(path, name); diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java b/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java index 148a841ac..d50028421 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/reload/CoreReloadListenersContainer.java @@ -1,7 +1,5 @@ package org.github.gestalt.config.reload; -import java.util.ArrayList; -import java.util.List; import java.util.WeakHashMap; /** diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java index 6f37e47ac..6c6c1bf77 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ProxyDecoderTest.java @@ -11,7 +11,6 @@ import org.github.gestalt.config.path.mapper.StandardPathMapper; import org.github.gestalt.config.reflect.TypeCapture; import org.github.gestalt.config.reload.ManualConfigReloadStrategy; -import org.github.gestalt.config.source.MapConfigSource; import org.github.gestalt.config.source.MapConfigSourceBuilder; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.test.classes.*;