package fr.xephi.authme.initialization;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import javax.annotation.PostConstruct;
import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Dependency injector of AuthMe: initializes and injects services and tasks.
*
* Only constructor and field injection are supported, indicated with the JSR330
* {@link javax.inject.Inject @Inject} annotation.
*
* {@link PostConstruct @PostConstruct} methods are recognized and invoked upon
* instantiation. Note that the parent classes are not scanned for such methods.
*/
public class AuthMeServiceInitializer {
private final Set ALLOWED_PACKAGES;
private final Map, Object> objects;
/**
* Constructor.
*
* @param allowedPackages list of allowed packages. Only classes whose package
* starts with any of the given entries will be instantiated
*/
public AuthMeServiceInitializer(String... allowedPackages) {
ALLOWED_PACKAGES = ImmutableSet.copyOf(allowedPackages);
objects = new HashMap<>();
objects.put(getClass(), this);
}
/**
* Retrieves or instantiates an object of the given type.
*
* @param clazz the class to retrieve the value for
* @param the class' type
* @return object of the class' type
*/
public T get(Class clazz) {
return get(clazz, new HashSet>());
}
/**
* Register an object with a custom class (supertype). Use this for example to specify a
* concrete implementation of an interface or an abstract class.
*
* @param clazz the class to register the object for
* @param object the object
* @param the class' type
*/
public void register(Class super T> clazz, T object) {
if (objects.containsKey(clazz)) {
throw new IllegalStateException("There is already an object present for " + clazz);
}
Preconditions.checkNotNull(object);
objects.put(clazz, object);
}
/**
* Associate an annotation with a value.
*
* @param annotation the annotation
* @param value the value
*/
public void provide(Class extends Annotation> annotation, Object value) {
if (objects.containsKey(annotation)) {
throw new IllegalStateException("Annotation @" + annotation.getClass().getSimpleName()
+ " already registered");
}
Preconditions.checkNotNull(value);
objects.put(annotation, value);
}
/**
* Creates a new instance of the given class and does not keep track of it afterwards,
* unlike {@link #get(Class)} (singleton scope).
*
* @param clazz the class to instantiate
* @param the class' type
* @return new instance of class T
*/
public T newInstance(Class clazz) {
return instantiate(clazz, new HashSet>());
}
/**
* Returns an instance of the given class or the value associated with an annotation,
* by retrieving it or by instantiating it if not yet present.
*
* @param clazz the class to retrieve a value for
* @param traversedClasses the list of traversed classes
* @param the class' type
* @return instance or associated value (for annotations)
*/
private T get(Class clazz, Set> traversedClasses) {
if (Annotation.class.isAssignableFrom(clazz)) {
throw new UnsupportedOperationException("Cannot retrieve annotated elements in this way!");
} else if (objects.containsKey(clazz)) {
return clazz.cast(objects.get(clazz));
}
// First time we come across clazz, need to instantiate it. Validate that we can do so
validatePackage(clazz);
validateInstantiable(clazz);
// Add the clazz to the list of traversed classes in a new Set, so each path we take has its own Set.
traversedClasses = new HashSet<>(traversedClasses);
traversedClasses.add(clazz);
T object = instantiate(clazz, traversedClasses);
storeObject(object);
return object;
}
/**
* Instantiates the given class by locating an @Inject constructor and retrieving
* or instantiating its parameters.
*
* @param clazz the class to instantiate
* @param traversedClasses collection of classes already traversed
* @param the class' type
* @return the instantiated object
*/
private T instantiate(Class clazz, Set> traversedClasses) {
Injection injection = firstNotNull(ConstructorInjection.provide(clazz), FieldInjection.provide(clazz));
if (injection == null) {
throw new IllegalStateException("Did not find injection method for " + clazz + ". Make sure you have "
+ "a constructor with @Inject or fields with @Inject. Fields with @Inject require "
+ "the default constructor");
}
validateInjectionHasNoCircularDependencies(injection.getDependencies(), traversedClasses);
Object[] dependencies = resolveDependencies(injection, traversedClasses);
T object = injection.instantiateWith(dependencies);
executePostConstructMethods(object);
return object;
}
/**
* Resolves the dependencies for the given constructor, i.e. returns a collection that satisfy
* the constructor's parameter types by retrieving elements or instantiating them where necessary.
*
* @param injection the injection parameters
* @param traversedClasses collection of traversed classes
* @return array with the parameters to use in the constructor
*/
private Object[] resolveDependencies(Injection> injection, Set> traversedClasses) {
Class>[] dependencies = injection.getDependencies();
Class>[] annotations = injection.getDependencyAnnotations();
Object[] values = new Object[dependencies.length];
for (int i = 0; i < dependencies.length; ++i) {
if (annotations[i] != null) {
Object value = objects.get(annotations[i]);
if (value == null) {
throw new IllegalStateException("Value for field with @" + annotations[i].getSimpleName()
+ " must be registered beforehand");
}
values[i] = value;
} else {
values[i] = get(dependencies[i], traversedClasses);
}
}
return values;
}
/**
* Stores the given object with its class as key. Throws an exception if the key already has
* a value associated to it.
*
* @param object the object to store
*/
private void storeObject(Object object) {
if (objects.containsKey(object.getClass())) {
throw new IllegalStateException("There is already an object present for " + object.getClass());
}
Preconditions.checkNotNull(object);
objects.put(object.getClass(), object);
}
/**
* Validates that none of the dependencies' types are present in the given collection
* of traversed classes. This prevents circular dependencies.
*
* @param dependencies the dependencies of the class
* @param traversedClasses the collection of traversed classes
*/
private static void validateInjectionHasNoCircularDependencies(Class>[] dependencies,
Set> traversedClasses) {
for (Class> clazz : dependencies) {
if (traversedClasses.contains(clazz)) {
throw new IllegalStateException("Found cyclic dependency - already traversed '" + clazz
+ "' (full traversal list: " + traversedClasses + ")");
}
}
}
/**
* Validates the package of a parameter type to ensure that it is part of the allowed packages.
* This ensures that we don't try to instantiate things that are beyond our reach in case some
* external parameter type has not been registered.
*
* @param clazz the class to validate
*/
private void validatePackage(Class> clazz) {
if (clazz.getPackage() == null) {
throw new IllegalStateException("Primitive types must be provided explicitly (or use an annotation).");
}
String packageName = clazz.getPackage().getName();
for (String allowedPackage : ALLOWED_PACKAGES) {
if (packageName.startsWith(allowedPackage)) {
return;
}
}
throw new IllegalStateException("Class " + clazz + " with package " + packageName + " is outside of the "
+ "allowed packages. It must be provided explicitly or the package must be passed to the constructor.");
}
private static void executePostConstructMethods(Object object) {
for (Method method : object.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(PostConstruct.class)) {
if (method.getParameterTypes().length == 0 && !Modifier.isStatic(method.getModifiers())) {
try {
method.setAccessible(true);
method.invoke(object);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new UnsupportedOperationException(e);
}
} else {
throw new IllegalStateException(String.format("@PostConstruct methods may not be static or have "
+ " any parameters. Method '%s' of class '%s' is either static or has parameters",
method.getName(), object.getClass().getSimpleName()));
}
}
}
}
private static void validateInstantiable(Class> clazz) {
if (clazz.isEnum() || clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
throw new IllegalStateException("Class " + clazz.getSimpleName() + " cannot be instantiated");
}
}
@SafeVarargs
private static Injection firstNotNull(Provider extends Injection>... providers) {
for (Provider extends Injection> provider : providers) {
Injection object = provider.get();
if (object != null) {
return object;
}
}
return null;
}
}