Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/proxy reload #133

Merged
merged 3 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand All @@ -28,10 +29,12 @@
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class ProxyDecoder implements Decoder<Object> {

// 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) {

private static String getConfigNameFromMethod(String methodName, Type returnType) {
String name = methodName;
if (methodName.startsWith("get")) {
name = methodName.substring(3);
Expand Down Expand Up @@ -104,7 +107,7 @@ public ValidateOf<Object> 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);
Expand Down Expand Up @@ -152,7 +155,10 @@ public ValidateOf<Object> decode(String path, Tags tags, ConfigNode node, TypeCa

case CACHE:
default: {
proxyHandler = new ProxyCacheInvocationHandler(path, methodResults);
proxyHandler = new ProxyCacheInvocationHandler(path, tags, decoderContext, methodResults);
if(decoderContext.getGestalt() != null) {
decoderContext.getGestalt().registerListener((ProxyCacheInvocationHandler) proxyHandler);
}
break;
}
}
Expand All @@ -161,46 +167,11 @@ public ValidateOf<Object> 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<String, Object> methodResults;


private ProxyCacheInvocationHandler(String path, Map<String, Object> 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;
static class ProxyPassThroughInvocationHandler implements InvocationHandler {
protected final String path;
protected final Tags tags;
protected final DecoderContext decoderContext;


private ProxyPassThroughInvocationHandler(String path, Tags tags, DecoderContext decoderContext) {
Expand All @@ -211,6 +182,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<Object> 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<Object> retrieveConfig(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
boolean isDefault = method.isDefault();

Expand All @@ -224,15 +205,18 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
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);

var result = decoderContext.getGestalt().getConfigOptional(nextPath, TypeCapture.of(genericType), tags);
Optional<Object> 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.
Expand All @@ -243,23 +227,64 @@ 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,
MethodType.methodType(returnType, new Class[0]),
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();
}
}


static class ProxyCacheInvocationHandler extends ProxyPassThroughInvocationHandler
implements InvocationHandler, CoreReloadListener {

private static final System.Logger logger = System.getLogger(ProxyCacheInvocationHandler.class.getName());
private final Map<String, Object> methodResults;


private ProxyCacheInvocationHandler(String path, Tags tags, DecoderContext decoderContext, Map<String, Object> 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<Object> 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;
}
}

@Override
public void reload() {
logger.log(System.Logger.Level.DEBUG, "Reloading received on Proxy Cache Listener. Clearing Cache");
methodResults.clear();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.github.gestalt.config.reload;

import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

/**
* Store all core reload listeners and functionality to call the on reload.
Expand All @@ -12,7 +11,7 @@ public class CoreReloadListenersContainer {
/**
* Listeners for the core reload.
*/
protected final List<CoreReloadListener> listeners = new ArrayList<>();
protected final WeakHashMap<Integer, CoreReloadListener> listeners = new WeakHashMap<>();

/**
* Default constructor for CoreReloadStrategy.
Expand All @@ -26,7 +25,7 @@ public CoreReloadListenersContainer() {
* @param listener to register
*/
public void registerListener(CoreReloadListener listener) {
listeners.add(listener);
listeners.put(listener.hashCode(), listener);
}

/**
Expand All @@ -35,13 +34,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());
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,6 +10,8 @@
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.MapConfigSourceBuilder;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.test.classes.*;
import org.github.gestalt.config.utils.ValidateOf;
Expand Down Expand Up @@ -520,5 +524,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<String, String> 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<String, String> 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<String, String> 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());
}
}