ljacqu/DependencyInjector

View on GitHub
injector/src/main/java/ch/jalu/injector/handlers/instantiation/StandardInjectionProvider.java

Summary

Maintainability
A
0 mins
Test Coverage
package ch.jalu.injector.handlers.instantiation;

import ch.jalu.injector.exceptions.InjectorException;
import ch.jalu.injector.utils.ReflectionUtils;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;

/**
 * Provider of {@link Resolution} objects that roughly follows the documentation as given in {@link Inject}.
 * The following is a summary of the instantiation's behavior and its deviations.
 * <p>
 * If available, the constructor with @{@link Inject} is used. Multiple {@code @Inject} constructors results in
 * an exception being thrown. Otherwise, if there is only one constructor, of public visibility taking no arguments,
 * it is taken. For private classes (inner classes), a sole no-args constructor is also considered.
 * Finally, the no-args constructor of any visibility is taken if it belongs to a class that has {@code @Inject}
 * fields. In this case, the no-args constructor is not required to be the only constructor of the class.
 * <p>
 * Fields with {@code @Inject} are injected. They may be static (not recommended). If a final field is annotated
 * with {@code @Inject}, an exception is thrown.
 * <p>
 * Method injection is not supported. Consequently, if a method with {@code @Inject} is found on the class or
 * any of its parents, an exception is thrown.
 * <p>
 * {@link ch.jalu.injector.annotations.NoMethodScan} and {@link ch.jalu.injector.annotations.NoFieldScan} are respected.
 */
public class StandardInjectionProvider extends DirectInstantiationProvider {

    @Override
    public <T> Resolution<T> safeGet(Class<T> clazz) {
        Constructor<T> constructor = getInjectionConstructor(clazz);
        if (constructor == null) {
            return null;
        }

        List<Field> fields = getFieldsToInject(clazz);
        validateInjection(clazz, constructor, fields);
        return new StandardInjection<>(constructor, fields);
    }

    // -------------
    // Constructors
    // -------------

    /**
     * Returns the constructor to be used for injection. Throws an exception if there
     * are multiple {@code @Inject} constructors.
     *
     * @param clazz the class to process
     * @param <T> the class's type
     * @return the constructor, or {@code null} if there is no constructor suitable for injection
     */
    @Nullable
    protected <T> Constructor<T> getInjectionConstructor(Class<T> clazz) {
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        if (constructors.length == 1 && isSuitableNoArgsConstructor(constructors[0])) {
            return (Constructor<T>) constructors[0];
        }

        Constructor<?> matchingConstructor = null;
        for (Constructor<?> constructor : constructors) {
            if (constructor.isAnnotationPresent(Inject.class)) {
                if (matchingConstructor != null) {
                    throw new InjectorException("Class '" + clazz + "' may not have multiple @Inject constructors");
                }
                matchingConstructor = constructor;
            }
        }

        if (matchingConstructor == null) {
            // Compatibility: If a class has at least one field with @Inject, take a non-public no-args constructor
            return getNoArgsConstructorIfHasInjectField(clazz);
        }
        return (Constructor<T>) matchingConstructor;
    }

    private static boolean isSuitableNoArgsConstructor(Constructor<?> c) {
        if (c.getParameterTypes().length > 0) {
            return false;
        }
        return !Modifier.isPrivate(c.getModifiers()) || Modifier.isPrivate(c.getDeclaringClass().getModifiers());
    }

    @Nullable
    private static <T> Constructor<T> getNoArgsConstructorIfHasInjectField(Class<T> clazz) {
        try {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            for (Field field : ReflectionUtils.safeGetDeclaredFields(clazz)) {
                if (field.isAnnotationPresent(Inject.class)) {
                    return (Constructor<T>) constructor;
                }
            }
        } catch (NoSuchMethodException e) {
            // noop
        }
        return null;
    }

    // ------------
    // Fields
    // ------------

    /**
     * Returns the fields in the class and its parents that should be injected.
     *
     * @param clazz the class to process
     * @return the fields to inject
     */
    protected List<Field> getFieldsToInject(Class<?> clazz) {
        List<Field> fields = new LinkedList<>();
        Class<?> currentClass = clazz;
        while (currentClass != null) {
            for (Field f : ReflectionUtils.safeGetDeclaredFields(currentClass)) {
                if (f.isAnnotationPresent(Inject.class)) {
                    fields.add(f);
                }
            }
            currentClass = currentClass.getSuperclass();
        }
        return fields;
    }

    // ------------
    // Validation
    // ------------

    /**
     * Assures that the class and the members relevant for the instantiation form a valid combination.
     *
     * @param clazz the class to instantiate
     * @param constructor the constructor to instantiate with
     * @param fields the fields to inject
     */
    protected void validateInjection(Class<?> clazz, Constructor<?> constructor, List<Field> fields) {
        validateHasNoFinalFields(fields);
        validateHasNoInjectMethods(clazz);
    }

    private void validateHasNoFinalFields(List<Field> fields) {
        for (Field field : fields) {
            if (Modifier.isFinal(field.getModifiers())) {
                throw new InjectorException("Field '" + field + "' may not be final and have @Inject");
            }
        }
    }

    private void validateHasNoInjectMethods(Class<?> clazz) {
        Class<?> currentClass = clazz;
        while (currentClass != null) {
            for (Method m : ReflectionUtils.safeGetDeclaredMethods(currentClass)) {
                if (m.isAnnotationPresent(Inject.class)) {
                    throw new InjectorException("@Inject on methods is not supported, but found it on '" + m
                        + "' while trying to instantiate '" + currentClass + "'");
                }
            }
            currentClass = currentClass.getSuperclass();
        }
    }
}