227 lines
8.4 KiB
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;
|
|
}
|
|
}
|