Files
game/src/main/java/cz/jzitnik/utils/DependencyManager.java

227 lines
8.4 KiB
Java

package cz.jzitnik.utils;
// Don't blame me that I'm using field injection instead of construction injection. I just like it more, leave me alone.
// Yes, I know I'll suffer in the unit tests. (who said there will be any? hmmm)
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;
import cz.jzitnik.annotations.Config;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.PostInit;
import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
@Slf4j
public class DependencyManager extends InjectableValues {
private final ClassToInstanceMap<Object> configs = MutableClassToInstanceMap.create();
private final ClassToInstanceMap<Object> data = MutableClassToInstanceMap.create();
public <T> T getDependencyOrThrow(Class<T> clazz) {
T instance = data.getInstance(clazz);
if (instance == null) {
throw new RuntimeException("Class was not found!");
}
return instance;
}
public <T> Optional<T> getDependency(Class<T> clazz) {
return Optional.ofNullable(data.getInstance(clazz));
}
public <T> Optional<T> getConfig(Class<T> clazz) {
return Optional.ofNullable(configs.getInstance(clazz));
}
public <T> T getConfigOrThrow(Class<T> clazz) {
T instance = configs.getInstance(clazz);
if (instance == null) {
throw new RuntimeException("Class was not found!");
}
return instance;
}
public DependencyManager(Reflections reflections) {
Set<Class<?>> configClasses = reflections.getTypesAnnotatedWith(Config.class);
for (Class<?> configClass : configClasses) {
try {
Constructor<?> constructor = configClass.getDeclaredConstructor();
var instance = constructor.newInstance();
configs.put(configClass, instance);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
log.error("Failed to instantiate config class: {}", configClass.getName(), e);
}
}
data.put(ClassLoader.class, DependencyManager.class.getClassLoader());
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Dependency.class);
// Construct all classes
for (Class<?> clazz : classes) {
for (var constructor : clazz.getDeclaredConstructors()) {
var paramTypes = constructor.getParameterTypes();
var params = new Object[paramTypes.length];
boolean suitable = true;
for (int i = 0; i < paramTypes.length; i++) {
Class<?> type = paramTypes[i];
if (type == getClass())
params[i] = this;
else if (type == Reflections.class)
params[i] = reflections;
else {
suitable = false;
break;
}
}
if (!suitable) continue;
constructor.setAccessible(true);
try {
Object instance = constructor.newInstance(params);
Dependency annotation = clazz.getAnnotation(Dependency.class);
if (annotation.value() != Object.class) {
data.put(annotation.value(), instance);
} else {
data.put(clazz, instance);
}
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
break; // Found a matching constructor, go to the next class
}
}
for (Object instance : data.values()) {
inject(instance);
}
}
public void inject(Object instance) {
StateManager stateManager = (StateManager) data.get(StateManager.class);
List<Field> allFields = new ArrayList<>();
Class<?> current = instance.getClass();
while (current != null && current != Object.class) {
allFields.addAll(Arrays.asList(current.getDeclaredFields()));
current = current.getSuperclass();
}
for (Field field : allFields) {
if (field.isAnnotationPresent(InjectDependency.class)) {
field.setAccessible(true);
if (!data.containsKey(field.getType()) && field.getType() != getClass()) continue;
Object dependency = field.getType() == getClass() ? this : data.get(field.getType());
if (!field.getType().isAssignableFrom(dependency.getClass())) continue;
try {
field.set(instance, dependency);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} else if (field.isAnnotationPresent(InjectState.class)) {
field.setAccessible(true);
Optional<?> stateOptional = stateManager.get(field.getType());
if (stateOptional.isEmpty()) continue;
try {
field.set(instance, stateOptional.get());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} else if (field.isAnnotationPresent(InjectConfig.class)) {
field.setAccessible(true);
Optional<?> config = Optional.ofNullable(configs.get(field.getType()));
if (config.isEmpty()) continue;
try {
field.set(instance, config.get());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
for (Method method : instance.getClass().getDeclaredMethods()) {
if (!method.isAnnotationPresent(PostInit.class)) {
continue;
}
if (method.getParameterCount() != 0) {
throw new IllegalStateException("@PostInit method must have no parameters: " + method);
}
try {
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(
"Failed to invoke @PostInit method: " + method, e
);
}
}
}
@Override
public Object findInjectableValue(Object valueId,
DeserializationContext ctxt,
BeanProperty forProperty,
Object beanInstance) throws JsonMappingException {
if (valueId instanceof Class<?> clazz) {
Object dep = data.getInstance(clazz);
if (dep != null) return dep;
dep = configs.getInstance(clazz);
if (dep != null) return dep;
if (clazz == this.getClass()) return this;
ctxt.reportInputMismatch(forProperty,
"No injectable value found for type: %s", clazz.getName());
} else if (valueId instanceof String key) {
for (Object dep: data.values()) {
if (dep.getClass().getName().equalsIgnoreCase(key)) return dep;
}
for (Object dep : configs.values()) {
if (dep.getClass().getName().equalsIgnoreCase(key)) return dep;
}
ctxt.reportInputMismatch(forProperty,
"No injectable value found for key: %s", key);
} else {
ctxt.reportInputMismatch(forProperty,
"Unrecognized inject value id type (%s), expecting Class or String",
valueId.getClass().getName());
}
return null;
}
}