ljacqu/DependencyInjector

View on GitHub
injector/src/main/java/ch/jalu/injector/InjectorImpl.java

Summary

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

import ch.jalu.injector.context.ObjectIdentifier;
import ch.jalu.injector.context.ResolutionContext;
import ch.jalu.injector.context.ResolutionType;
import ch.jalu.injector.context.StandardResolutionType;
import ch.jalu.injector.exceptions.InjectorException;
import ch.jalu.injector.handlers.Handler;
import ch.jalu.injector.handlers.instantiation.Resolution;
import ch.jalu.injector.utils.InjectorUtils;

import javax.annotation.Nullable;
import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static ch.jalu.injector.context.StandardResolutionType.REQUEST_SCOPED;
import static ch.jalu.injector.context.StandardResolutionType.REQUEST_SCOPED_IF_HAS_DEPENDENCIES;
import static ch.jalu.injector.context.StandardResolutionType.SINGLETON;
import static ch.jalu.injector.utils.InjectorUtils.checkNotNull;
import static ch.jalu.injector.utils.InjectorUtils.containsNullValue;
import static ch.jalu.injector.utils.InjectorUtils.firstNotNull;
import static ch.jalu.injector.utils.InjectorUtils.rethrowException;

/**
 * Implementation of {@link Injector}.
 */
public class InjectorImpl implements Injector {

    protected Map<Class<?>, Object> objects;
    protected InjectorConfig config;

    /**
     * Constructor.
     *
     * @param config injector configuration
     * @see InjectorBuilder
     */
    protected InjectorImpl(InjectorConfig config) {
        this.config = config;
        this.objects = new HashMap<>();
        this.objects.put(Injector.class, this);
    }

    @Override
    public <T> void register(Class<? super T> clazz, T object) {
        if (objects.containsKey(clazz)) {
            throw new InjectorException("There is already an object present for " + clazz);
        }
        checkNotNull(object);
        objects.put(clazz, object);
    }

    @Override
    public void provide(Class<? extends Annotation> clazz, Object object) {
        checkNotNull(clazz, "Provided annotation may not be null");
        try {
            for (Handler handler : config.getHandlers()) {
                handler.onAnnotation(clazz, object);
            }
        } catch (Exception e) {
            rethrowException(e);
        }
    }

    @Override
    public <T> T getSingleton(Class<T> clazz) {
        return resolve(SINGLETON, clazz);
    }

    @Override
    public <T> T newInstance(Class<T> clazz) {
        return resolve(REQUEST_SCOPED, clazz);
    }

    @Override
    public <T> T getIfAvailable(Class<T> clazz) {
        return clazz.cast(objects.get(clazz));
    }

    @Override
    public <T> T createIfHasDependencies(Class<T> clazz) {
        return resolve(REQUEST_SCOPED_IF_HAS_DEPENDENCIES, clazz);
    }

    @Override
    public <T> Collection<T> retrieveAllOfType(Class<T> clazz) {
        List<T> instances = new ArrayList<>();
        for (Object object : objects.values()) {
            if (clazz.isInstance(object)) {
                instances.add(clazz.cast(object));
            }
        }
        return instances;
    }

    @Override
    public <T> void registerProvider(Class<T> clazz, Provider<? extends T> provider) {
        checkNotNull(clazz, "Class may not be null");
        checkNotNull(provider, "Provider may not be null");
        try {
            for (Handler handler : config.getHandlers()) {
                handler.onProvider(clazz, provider);
            }
        } catch (Exception e) {
            rethrowException(e);
        }
    }

    @Override
    public <T, P extends Provider<? extends T>> void registerProvider(Class<T> clazz, Class<P> providerClass) {
        checkNotNull(clazz, "Class may not be null");
        checkNotNull(providerClass, "Provider class may not be null");
        try {
            for (Handler handler : config.getHandlers()) {
                handler.onProviderClass(clazz, providerClass);
            }
        } catch (Exception e) {
            rethrowException(e);
        }
    }

    public InjectorConfig getConfig() {
        return config;
    }

    @SuppressWarnings("unchecked")
    private <T> T resolve(ResolutionType resolutionType, Class<?> clazz) {
        return (T) resolveContext(
            new ResolutionContext(this, new ObjectIdentifier(resolutionType, clazz)));
    }

    /**
     * Returns the object as defined by the given context.
     *
     * @param context the context to resolve the object for
     * @return the resolved object, {@code null} if the context specifies it is optional and some criteria is not met
     */
    @Nullable
    protected Object resolveContext(ResolutionContext context) {
        // TODO #49: Convert singleton store to a Handler impl.
        if (context.getIdentifier().getResolutionType() == StandardResolutionType.SINGLETON) {
            Object knownSingleton = objects.get(context.getIdentifier().getTypeAsClass());
            if (knownSingleton != null) {
                return knownSingleton;
            }
        }

        Resolution<?> resolution = findResolutionOrFail(context);
        if (isContextChildOfOptionalRequest(context) && resolution.isInstantiation()) {
            return null;
        }

        Object[] resolvedDependencies = resolveDependencies(context, resolution);
        if (containsNullValue(resolvedDependencies)) {
            throwForUnexpectedNullDependency(context);
            return null;
        }

        Object object = runPostConstructHandlers(resolution.instantiateWith(resolvedDependencies), context, resolution);
        if (resolution.isInstantiation() && context.getIdentifier().getResolutionType() == SINGLETON) {
            register((Class) context.getOriginalIdentifier().getTypeAsClass(), object);
        }
        return object;
    }

    /**
     * Resolves the dependencies as defined by the given resolution.
     * If a dependency is resolved to {@code null}, the process is aborted and the remaining dependencies
     * are not resolved.
     *
     * @param context the resolution context
     * @param resolution the resolution whose dependencies should be provided
     * @return array with the dependencies, in the same order as given by the resolution
     */
    protected Object[] resolveDependencies(ResolutionContext context, Resolution<?> resolution) {
        final int totalDependencies = resolution.getDependencies().size();
        final Object[] resolvedDependencies = new Object[totalDependencies];

        int index = 0;
        for (ObjectIdentifier dependencyId : resolution.getDependencies()) {
            Object dependency = resolveContext(context.createChildContext(dependencyId));
            if (dependency == null) {
                break;
            }
            resolvedDependencies[index] = dependency;
            ++index;
        }
        return resolvedDependencies;
    }

    /**
     * Called when a resolved dependency is null, this method may throw an exception in the cases when this
     * should not happen. If this method does not throw an exception, null is returned from {@link #resolveContext}.
     *
     * @param context the resolution context
     */
    protected void throwForUnexpectedNullDependency(ResolutionContext context) {
        if (context.getIdentifier().getResolutionType() == REQUEST_SCOPED_IF_HAS_DEPENDENCIES
            || isContextChildOfOptionalRequest(context)) {
            // Situation where null may occur, so throw no exception
            return;
        }
        throw new InjectorException("Found null returned as dependency while resolving '"
            + context.getIdentifier() + "'");
    }

    private static boolean isContextChildOfOptionalRequest(ResolutionContext context) {
        return !context.getParents().isEmpty()
            && context.getParents().get(0).getIdentifier().getResolutionType() == REQUEST_SCOPED_IF_HAS_DEPENDENCIES;
    }

    /**
     * Calls the defined handlers and returns the first {@link Resolution} that is returned based on
     * the provided resolution context. Throws an exception if no handler returned a resolution.
     *
     * @param context the context to find the resolution for
     * @return the resolution
     */
    protected Resolution<?> findResolutionOrFail(ResolutionContext context) {
        try {
            for (Handler handler : config.getHandlers()) {
                Resolution<?> resolution = handler.resolve(context);
                if (resolution != null) {
                    return resolution;
                }
            }
        } catch (Exception e) {
            rethrowException(e);
        }

        final Class<?> clazz = context.getIdentifier().getTypeAsClass();
        if (!InjectorUtils.canInstantiate(clazz)) {
            String hint = clazz.isPrimitive()
                ? "Primitive types must be provided by default. "
                : clazz.isArray()
                    ? "By default, arrays cannot be injected. "
                    : "";
            throw new InjectorException(String.format("Did not find instantiation method for '%s'."
                    + " %sThis class cannot be instantiated; please check the class or your handlers.",
                    clazz, hint));
        }
        throw new InjectorException("Did not find instantiation method for '" + context.getIdentifier().getTypeAsClass()
            + "'. Make sure your class conforms to one of the registered instantiations. If default: "
            + "make sure you have a constructor with @Inject or fields with @Inject. Fields with @Inject "
            + "require the default constructor");
    }

    /**
     * Invokes the handler's post construct method when appropriate. Returns the object as returned by the
     * handlers, which may be different from the provided one.
     *
     * @param instance the object that was resolved
     * @param context the resolution context
     * @param resolution the resolution used to get the object
     * @param <T> the object's type
     * @return the object to use (as post construct methods may change it)
     */
    protected <T> T runPostConstructHandlers(T instance, ResolutionContext context, Resolution<?> resolution) {
        if (!resolution.isInstantiation()) {
            return instance;
        }

        T object = instance;
        try {
            for (Handler handler : config.getHandlers()) {
                object = firstNotNull(handler.postProcess(object, context, resolution), object);
            }
        } catch (Exception e) {
            rethrowException(e);
        }
        return object;
    }
}