All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.opsbears.webcomponents.dic.InjectorConfiguration Maven / Gradle / Ivy

package com.opsbears.webcomponents.dic;

import com.opsbears.webcomponents.immutable.ImmutableArrayList;
import com.opsbears.webcomponents.immutable.ImmutableHashMap;
import com.opsbears.webcomponents.immutable.ImmutableList;
import com.opsbears.webcomponents.immutable.ImmutableMap;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.concurrent.Immutable;
import javax.inject.Provider;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

/**
 * Contains the full configuration that is to be used for dependency injection.
 */
@Immutable
@ParametersAreNonnullByDefault
public class InjectorConfiguration {
    private final ImmutableList scopes;
    /**
     * List of classes that are legal to instantiate.
     */
    private final ImmutableScope>> definedClasses;
    /**
     * List of classes that should be instantiated using factories.
     */
    private final ImmutableScope> factories;
    /**
     * List of classes that should be instantiated using factories.
     */
    private final ImmutableScope>>> factoryClasses;
    /**
     * List of shared classes that should be cached on first instantiation.
     */
    private final ImmutableScope> sharedClasses;
    /**
     * List of pre-instantiated shared objects.
     */
    private final ImmutableMap sharedInstances;
    /**
     * List of interface/class aliases.
     */
    private final ImmutableScope> aliases;
    private final ImmutableScope>> collectedAliases;
    /**
     * List of named parameter values.
     */
    private final ImmutableScope> namedParameterValues;

    public InjectorConfiguration() {
        scopes = new ImmutableArrayList<>();
        definedClasses = new ImmutableScope<>(new ImmutableHashMap<>());
        factories = new ImmutableScope<>(new ImmutableMapHierarchy<>());
        factoryClasses = new ImmutableScope<>(new ImmutableMapHierarchy<>());
        sharedClasses = new ImmutableScope<>(new ImmutableArrayList<>());
        sharedInstances = new ImmutableHashMap<>();
        aliases = new ImmutableScope<>(new ImmutableMapHierarchy<>());
        collectedAliases = new ImmutableScope<>(new ImmutableHashMap<>());
        namedParameterValues = new ImmutableScope<>(new ImmutableMapHierarchy<>());
    }

    private InjectorConfiguration(
        ImmutableList scopes,
        ImmutableScope>> definedClasses,
        ImmutableScope> factories,
        ImmutableScope>>> factoryClasses,
        ImmutableScope> sharedClasses,
        ImmutableMap sharedInstances,
        ImmutableScope> aliases,
        ImmutableScope>> collectedAliases,
        ImmutableScope> namedParameterValues
    ) {
        this.scopes = scopes;
        this.definedClasses = definedClasses;
        this.factories = factories;
        this.factoryClasses = factoryClasses;
        this.sharedClasses = sharedClasses;
        this.sharedInstances = sharedInstances;
        this.aliases = aliases;
        this.collectedAliases = collectedAliases;
        this.namedParameterValues = namedParameterValues;
    }

    private void checkScope(Class scope) {
        if (!scopes.contains(scope)) {
            throw new RuntimeException("BUG: " + scope.getSimpleName() + " has not been declared as a scope.");
        }
    }

    public InjectorConfiguration withScope(Class scope) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes.withAdd(scope),
            definedClasses.withScopedValue(scope, new ImmutableHashMap<>()),
            factories.withScopedValue(scope, new ImmutableMapHierarchy<>()),
            factoryClasses.withScopedValue(scope, new ImmutableMapHierarchy<>()),
            sharedClasses.withScopedValue(scope, new ImmutableArrayList<>()),
            sharedInstances,
            aliases.withScopedValue(scope, new ImmutableMapHierarchy<>()),
            collectedAliases.withScopedValue(scope, new ImmutableHashMap<>()),
            namedParameterValues.withScopedValue(scope, new ImmutableMapHierarchy<>())
        );
    }

    /**
     * Marks a class as injectable on a global scope with all possible constructors.
     *
     * @param classDefinition the class to mark injectable
     *
     * @return a modified copy of this injection configuration.
     */
    public InjectorConfiguration withDefined(Class classDefinition) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses.withModified(value -> value.withPut(
                classDefinition,
                new ImmutableArrayList<>(Arrays.asList(classDefinition.getConstructors()))
            )),
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }

    /**
     * Marks a class as injectable on a global scope with all possible constructors.
     *
     * @param scope the scope to define this for.
     * @param classDefinition the class to mark injectable
     *
     * @return a modified copy of this injection configuration.
     *
     * @throws ScopeNotFound if the specified scope was not found.
     */
    public InjectorConfiguration withScopedDefined(Class scope, Class classDefinition) throws ScopeNotFound {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses.withModified(scope, value -> value.withPut(
                classDefinition,
                new ImmutableArrayList<>(Arrays.asList(classDefinition.getConstructors()))
            )),
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }

    /**
     * Marks a class as injectable on a global scope with one specific constructor.
     *
     * @param constructorDefinition the class to mark injectable
     *
     * @return a modified copy of this injection configuration.
     */
    public InjectorConfiguration withDefined(Constructor constructorDefinition) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses.withModified(
                scopeValue -> scopeValue
                    .withCompute(
                        constructorDefinition.getDeclaringClass(),
                        (key, currentValue) -> (
                            currentValue == null?
                                new ImmutableArrayList()
                                :
                                currentValue
                        ).withAdd(constructorDefinition)
                    )
            ),
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Marks a class as injectable on a global scope with one specific constructor.
     *
     * @param scope the scope to define this for.
     * @param constructorDefinition the class to mark injectable
     *
     * @return a modified copy of this injection configuration.
     *
     * @throws ScopeNotFound if the specified scope was not found.
     */
    public InjectorConfiguration withScopedDefined(Class scope, Constructor constructorDefinition) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses.withModified(
                scope,
                scopeValue -> scopeValue
                    .withCompute(
                        constructorDefinition.getDeclaringClass(),
                        (key, currentValue) -> (
                            currentValue == null?
                                new ImmutableArrayList()
                                :
                                currentValue
                        ).withAdd(constructorDefinition)
                    )
            ),
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Return a list of defined constructors for a certain class. May be empty.
     *
     * @param classDefinition the class that constructors are requested for.
     *
     * @return a list of constructors for the given class
     */
    public ImmutableList getConstructors(Class classDefinition) {
        if (definedClasses.getRootValue().containsKey(classDefinition)) {
            return definedClasses.getRootValue().get(classDefinition);
        }
        return new ImmutableArrayList<>();
    }


    /**
     * Return a list of defined constructors for a certain class. May be empty.
     *
     * @param scope the scope to return constructors for
     * @param classDefinition the class that constructors are requested for.
     *
     * @return a list of constructors for the given class
     *
     * @throws ScopeNotFound if the scope was not defined
     */
    public ImmutableList getScopedConstructors(Class scope, Class classDefinition) throws ScopeNotFound {
        ImmutableList constructors = getConstructors(classDefinition);

        ImmutableList scopedConstructors = definedClasses.getScope(scope).get(classDefinition);
        if (scopedConstructors != null) {
            constructors = constructors.withAddAll(scopedConstructors);
        }

        return constructors;
    }

    /**
     * Specify that the given class should be created using the factory instance specified.
     *
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Class classDefinition,
        Provider factory
    ) {
        if (classDefinition.equals(Injector.class)) {
            throw new DependencyInjectionFailedException("Cowardly refusing to define a global factory for Injector since that would lead to a Service Locator pattern. If you need the injector, please define it on a per-class or per-method basis.");
        }
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified((value) -> value.with(classDefinition, factory)),
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Specify that the given class should be created using the factory instance specified.
     *
     * @param scope the scipe of this factory
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedFactory(
        Class scope,
        Class classDefinition,
        Provider factory
    ) {
        if (classDefinition.equals(Injector.class)) {
            throw new DependencyInjectionFailedException("Cowardly refusing to define a global factory for Injector since that would lead to a Service Locator pattern. If you need the injector, please define it on a per-class or per-method basis.");
        }
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified((value) -> value.with(classDefinition, factory)),
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Specify that the given class should be created using the factory class specified. The factory class MUST be
     * instantiable via injection (e.g. defined or shared).
     *
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Class classDefinition,
        Class> factory
    ) {
        if (classDefinition.equals(Injector.class)) {
            throw new DependencyInjectionFailedException("Cowardly refusing to define a global factory for Injector since that would lead to a Service Locator pattern. If you need the injector, please define it on a per-class or per-method basis.");
        }
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses.withModified((value) -> value.with(classDefinition, (Class) factory)),
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Specify that the given class should be created using the factory instance specified, for a single class
     * instantiation only.
     *
     * @param scope the scope to apply this rule for
     * @param forClass the class this rule should apply for
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedFactory(
        Class scope,
        Class forClass,
        Class classDefinition,
        Provider factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified(scope, (value) -> value.with(forClass, classDefinition, factory)),
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Specify that the given class should be created using the factory instance specified, for a single class
     * instantiation only.
     *
     * @param forClass the class this rule should apply for
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Class forClass,
        Class classDefinition,
        Provider factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified((value) -> value.with(forClass, classDefinition, factory)),
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Specify that the given class should be created using the factory class specified, for a single class
     * instantiation only. The factory class MUST be instantiable via injection (e.g. defined or shared).
     *
     * @param forClass the class this rule should apply for
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedFactory(
        Class scope,
        Class forClass,
        Class classDefinition,
        Class> factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses.withModified(
                scope,
                (value) -> value.with(
                    forClass,
                    classDefinition,
                    (Class) factory
                )
            ),
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Specify that the given class should be created using the factory class specified, for a single class
     * instantiation only. The factory class MUST be instantiable via injection (e.g. defined or shared).
     *
     * @param forClass the class this rule should apply for
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Class forClass,
        Class classDefinition,
        Class> factory
    ) {
        //noinspection UnnecessaryLocalVariable
        Class newFactory = factory;

        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses.withModified(
                value -> value.with(
                    forClass,
                    classDefinition,
                    (Class>)newFactory
                )
            ),
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Specify that the given class should be created using the factory instance specified, for a single executable
     * only.
     *
     * @param forExecutable the executable this rule should apply for.
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedFactory(
        Class scope,
        Executable forExecutable,
        Class classDefinition,
        Provider factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified(scope, (value) -> value.with(forExecutable, classDefinition, factory)),
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }

    /**
     * Specify that the given class should be created using the factory instance specified, for a single executable
     * only.
     *
     * @param forExecutable the executable this rule should apply for.
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Executable forExecutable,
        Class classDefinition,
        Provider factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories.withModified((value) -> value.with(forExecutable, classDefinition, factory)),
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }


    /**
     * Specify that the given class should be created using the factory class specified, for a single executable
     * only.  The factory class MUST be instantiable via injection (e.g. defined or shared).
     *
     * @param forExecutable the executable this rule should apply for.
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedFactory(
        Class scope,
        Executable forExecutable,
        Class classDefinition,
        Class> factory
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses.withModified(scope, (value) -> value.with(forExecutable, classDefinition, (Class)factory)),
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Specify that the given class should be created using the factory class specified, for a single executable
     * only.  The factory class MUST be instantiable via injection (e.g. defined or shared).
     *
     * @param forExecutable the executable this rule should apply for.
     * @param classDefinition the class that should be produced using a factory.
     * @param factory the factory class that creates the class definition
     * @param  type of the class
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withFactory(
        Executable forExecutable,
        Class classDefinition,
        Class> factory
    ) {
        Class newFactory = factory;
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses.withModified(
                (value) -> value.with(
                    forExecutable,
                    classDefinition,
                    (Class>)newFactory
                )
            ),
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getScopedFactory(Class scope, Class classDefinition) {
        //noinspection unchecked
        return factories.getScope(scope).get(classDefinition);
    }

    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getFactory(Class classDefinition) {
        //noinspection unchecked
        return factories.getRootValue().get(classDefinition);
    }


    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param forClass the class this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getScopedFactory(Class scope, Class forClass, Class classDefinition) {
        //noinspection unchecked
        return factories.getScope(scope).get(forClass, classDefinition);
    }

    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param forClass the class this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getFactory(Class forClass, Class classDefinition) {
        //noinspection unchecked
        return factories.getRootValue().get(forClass, classDefinition);
    }


    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param forExecutable the executable this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getScopedFactory(Class scope, @Nullable Executable forExecutable, Class classDefinition) {
        //noinspection unchecked
        return factories.getScope(scope).get(forExecutable, classDefinition);
    }

    /**
     * Returns a factory instance to instantiate a certain class.
     *
     * @param forExecutable the executable this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Provider getFactory(@Nullable Executable forExecutable, Class classDefinition) {
        //noinspection unchecked
        return factories.getRootValue().get(forExecutable, classDefinition);
    }

    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getScopedFactoryClass(Class scope, Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getScope(scope).get(classDefinition);
    }

    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getFactoryClass(Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getRootValue().get(classDefinition);
    }


    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param forClass the class this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getScopedFactoryClass(Class scope, Class forClass, Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getScope(scope).get(forClass, classDefinition);
    }

    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param forClass the class this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getFactoryClass(Class forClass, Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getRootValue().get(forClass, classDefinition);
    }


    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param forExecutable the executable this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getScopedFactoryClass(Class scope, @Nullable Executable forExecutable, Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getScope(scope).get(forExecutable, classDefinition);
    }

    /**
     * Returns a factory class to instantiate a certain class.
     *
     * @param forExecutable the executable this factory is needed for.
     * @param classDefinition the class that is to be instantiated.
     * @param  the type of the class to be instantiated.
     *
     * @return a factory method for the specified class.
     */
    @Nullable
    public  Class> getFactoryClass(@Nullable Executable forExecutable, Class classDefinition) {
        //noinspection unchecked
        return (Class)factoryClasses.getRootValue().get(forExecutable, classDefinition);
    }


    /**
     * Marks a class definition as injectable and shared on a global scope. the first time this class definition will
     * be instantiated in an injector, it will be cached for subsequent calls. This comes with a couple of caveats:
     *
     * 1. the first instance of this class created, no matter in what specific context, may be cached.
     * 2. it is not guaranteed that this class will only be instantiated once. The injection process may take place in
     *    parallel threads, so instantiation may happen multiple times. However, only a single instance will be
     *    returned and retained.
     * 3. if you use multiple dependency injector copies, the shared instances may not be shared between them. To
     *    ensure singleton-ness, use the withShared method that accepts a pre-instantiated copy.
     *
     * @param classDefinition the class that should be shared.
     *
     * @return a modified copy of this injection configuration
     */
    public InjectorConfiguration withScopeShared(
        Class scope,
        Class classDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses.withModified(scope, (value) -> value.withAdd(classDefinition)),
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }


    /**
     * Marks a class definition as injectable and shared on a global scope. the first time this class definition will
     * be instantiated in an injector, it will be cached for subsequent calls. This comes with a couple of caveats:
     *
     * 1. the first instance of this class created, no matter in what specific context, may be cached.
     * 2. it is not guaranteed that this class will only be instantiated once. The injection process may take place in
     *    parallel threads, so instantiation may happen multiple times. However, only a single instance will be
     *    returned and retained.
     * 3. if you use multiple dependency injector copies, the shared instances may not be shared between them. To
     *    ensure singleton-ness, use the withShared method that accepts a pre-instantiated copy.
     *
     * @param classDefinition the class that should be shared.
     *
     * @return a modified copy of this injection configuration
     */
    public InjectorConfiguration withShared(
        Class classDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses.withModified((value) -> value.withAdd(classDefinition)),
            sharedInstances,
            aliases,
            collectedAliases, namedParameterValues
        );
    }


    /**
     * @return a list of classes that should be shared upon first instantiation.
     */
    public ImmutableList getScopedSharedClasses(Class scope) {
        return sharedClasses.getScope(scope);
    }


    /**
     * @return a list of classes that should be shared upon first instantiation.
     */
    public ImmutableList getSharedClasses() {
        return sharedClasses.getRootValue();
    }

    /**
     * Mark a class instance as shared. The type of the class will be marked as injectable and in all cases this
     * class instance will be passed when requested. This holds true even with multiple injectors as long as they
     * are created from the same configuration.
     *
     * @param instance class instance to share.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withShared(
        T instance
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances.withPut(instance.getClass(), instance),
            aliases,
            collectedAliases, namedParameterValues
        );
    }

    /**
     * @return a list of pre-initialized shared instances.
     */
    public ImmutableMap getSharedInstances() {
        return sharedInstances;
    }


    /**
     * Defines that instead of the class/interface passed in abstractDefinition, the class specified in
     * implementationDefinition should be used. The specified replacement class must be defined as injectable.
     *
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedAlias(
        Class scope,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        if (abstractDefinition.equals(Injector.class)) {
            throw new DependencyInjectionFailedException("Cowardly refusing to define a global alias for Injector since that would lead to a Service Locator pattern. If you need the injector, please define it on a per-class or per-method basis.");
        }
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases.withModified(scope, (value) -> value.with(abstractDefinition, implementationDefinition)),
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Defines that instead of the class/interface passed in abstractDefinition, the class specified in
     * implementationDefinition should be used. The specified replacement class must be defined as injectable.
     *
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withAlias(
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        if (abstractDefinition.equals(Injector.class)) {
            throw new DependencyInjectionFailedException("Cowardly refusing to define a global alias for Injector since that would lead to a Service Locator pattern. If you need the injector, please define it on a per-class or per-method basis.");
        }
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases.withModified((value) -> value.with(abstractDefinition, implementationDefinition)),
            collectedAliases,
            namedParameterValues
        );
    }


    /**
     * Defines on a per-class basis that instead of the class defined in `abstractDefinition` the class defined in
     * `implementationDefinition` should be used.
     *
     * @param forClass the class this alias should be used on.
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedAlias(
        Class scope,
        Class forClass,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases.withModified(scope, (value) -> value.with(forClass, abstractDefinition, implementationDefinition)),
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Defines on a per-class basis that instead of the class defined in `abstractDefinition` the class defined in
     * `implementationDefinition` should be used.
     *
     * @param forClass the class this alias should be used on.
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withAlias(
        Class forClass,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases.withModified((value) -> value.with(forClass, abstractDefinition, implementationDefinition)),
            collectedAliases,
            namedParameterValues
        );
    }

    /**
     * Defines on a per-method or per-constructor basis that instead of the class defined in `abstractDefinition` the
     * class defined in `implementationDefinition` should be used.
     *
     * @param forExecutable the constructor or method this alias should be used on.
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withAlias(
        Class scope,
        Executable forExecutable,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases.withModified(scope, (value) -> value.with(forExecutable, abstractDefinition, implementationDefinition)),
            collectedAliases, namedParameterValues
        );
    }

    /**
     * Defines on a per-method or per-constructor basis that instead of the class defined in `abstractDefinition` the
     * class defined in `implementationDefinition` should be used.
     *
     * @param forExecutable the constructor or method this alias should be used on.
     * @param abstractDefinition the abstract class or interface to replace.
     * @param implementationDefinition the implementation class
     * @param  type of the abstract class/interface
     * @param  type if the implementation
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withAlias(
        Executable forExecutable,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases.withModified((value) -> value.with(forExecutable, abstractDefinition, implementationDefinition)),
            collectedAliases, namedParameterValues
        );
    }

    /**
     * Returns an alias of abstractClass on a global scope, or null if no alias was found.
     *
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getScopedAlias(Class scope, Class abstractClass) {
        //noinspection unchecked
        return aliases.getScope(scope).get(abstractClass);
    }

    /**
     * Returns an alias of abstractClass on a global scope, or null if no alias was found.
     *
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getAlias(Class abstractClass) {
        //noinspection unchecked
        return aliases.getRootValue().get(abstractClass);
    }

    /**
     * Returns an alias of abstractClass on a class-scope, or null if no alias was found. The specified forClass is
     * the class for scoping purposes.
     *
     * @param forClass the class this alias is needed for.
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getScopedAlias(Class scope, Class forClass, Class abstractClass) {
        //noinspection unchecked
        return aliases.getScope(scope).get(forClass, abstractClass);
    }

    /**
     * Returns an alias of abstractClass on a class-scope, or null if no alias was found. The specified forClass is
     * the class for scoping purposes.
     *
     * @param forClass the class this alias is needed for.
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getAlias(Class forClass, Class abstractClass) {
        //noinspection unchecked
        return aliases.getRootValue().get(forClass, abstractClass);
    }

    /**
     * Returns an alias for `abstractClass` that should be used when executing the executable specified in `forExecutable`.
     *
     * @param forExecutable the executable this alias is needed for.
     *
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getScopedAlias(Class scope, @Nullable Executable forExecutable, Class abstractClass) {
        //noinspection unchecked
        return aliases.getScope(scope).get(forExecutable, abstractClass);
    }


    /**
     * Returns an alias for `abstractClass` that should be used when executing the executable specified in `forExecutable`.
     *
     * @param forExecutable the executable this alias is needed for.
     *
     * @param abstractClass the abstraction that the
     * @param  type of the abstract class or interface
     * @param  type of the implementation
     *
     * @return the class definition of the implementation.
     */
    @Nullable
    public  Class getAlias(@Nullable Executable forExecutable, Class abstractClass) {
        //noinspection unchecked
        return aliases.getRootValue().get(forExecutable, abstractClass);
    }

    /**
     * This method allows for specifying one _possible_ alias. Whenever a collection (list, set, etc) of the interface
     * is requested, the injector will instantiate all of these possibilities in order and pass the created list to the
     * instance.
     *
     * This is especially useful when creating a plugin-type scenario where different modules can supply implementations
     * to a specific interface.
     *
     * This method has no bearing on the scenario when a single instance is requested.
     *
     * @param abstractDefinition the abstraction to alias.
     * @param implementationDefinition the actual implementation to collect.
     * @param  the abstraction type.
     * @param  the implementation type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedCollectedAlias(
        Class scope,
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        ImmutableScope>> newCollectedAliases =
            collectedAliases.withModified(
                scope,
                (value) -> {
                    value = value.withPutIfAbsent(abstractDefinition, new ImmutableArrayList<>());
                    return value
                        .withPut(
                            abstractDefinition,
                            value
                                .get(abstractDefinition)
                                .withAdd(implementationDefinition)
                        );
                }
            );
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            newCollectedAliases,
            namedParameterValues
        );
    }

    /**
     * This method allows for specifying one _possible_ alias. Whenever a collection (list, set, etc) of the interface
     * is requested, the injector will instantiate all of these possibilities in order and pass the created list to the
     * instance.
     *
     * This is especially useful when creating a plugin-type scenario where different modules can supply implementations
     * to a specific interface.
     *
     * This method has no bearing on the scenario when a single instance is requested.
     *
     * @param abstractDefinition the abstraction to alias.
     * @param implementationDefinition the actual implementation to collect.
     * @param  the abstraction type.
     * @param  the implementation type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withCollectedAlias(
        Class abstractDefinition,
        Class implementationDefinition
    ) {
        //noinspection unchecked
        ImmutableScope>> newCollectedAliases =
            collectedAliases.withModified(
                (value) -> {
                    value = value.withPutIfAbsent(abstractDefinition, new ImmutableArrayList<>());
                    return value
                        .withPut(
                            abstractDefinition,
                            value
                                .get(abstractDefinition)
                                .withAdd(implementationDefinition)
                        );
                }
            );
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            newCollectedAliases,
            namedParameterValues
        );
    }

    public ImmutableList getScopedCollectedAliases(Class scope, Class classDefinition) {
        if (!collectedAliases.getScope(scope).containsKey(classDefinition)) {
            return new ImmutableArrayList<>();
        }
        return collectedAliases.getScope(scope).get(classDefinition);
    }

    public ImmutableList getCollectedAliases(Class classDefinition) {
        if (!collectedAliases.getRootValue().containsKey(classDefinition)) {
            return new ImmutableArrayList<>();
        }
        return collectedAliases.getRootValue().get(classDefinition);
    }


    /**
     * Set a certain value of a class parameter. When the specified class is instantiated and a parameter with the
     * specified name is encountered in the constructor, the specified value will be used. This only works if the
     * specified class was compiled with `-parameters` or the @javax.inject.Named annotation must be used.
     *
     * @param classDefinition The class that should receive the named parameter.
     * @param parameterName Parameter to look for.
     * @param value The exact value to pass.
     * @param  The value type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withScopedNamedParameterValue(
        Class scope,
        Class classDefinition,
        String parameterName,
        T value
    ) throws MissingNamedParameterSupport {
        //noinspection unchecked
        NamedParameterSupportChecker.checkNamedParameterSupport(classDefinition);
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues.withModified(
                scope,
                (v) -> v.with(classDefinition, parameterName, value)
            )
        );
    }

    /**
     * Set a certain value of a class parameter. When the specified class is instantiated and a parameter with the
     * specified name is encountered in the constructor, the specified value will be used. This only works if the
     * specified class was compiled with `-parameters` or the @javax.inject.Named annotation must be used.
     *
     * @param classDefinition The class that should receive the named parameter.
     * @param parameterName Parameter to look for.
     * @param value The exact value to pass.
     * @param  The value type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withNamedParameterValue(
        Class classDefinition,
        String parameterName,
        T value
    ) throws MissingNamedParameterSupport {
        //noinspection unchecked
        NamedParameterSupportChecker.checkNamedParameterSupport(classDefinition);
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses,
            sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues.withModified(
                (v) -> v.with(classDefinition, parameterName, value)
            )
        );
    }

    /**
     * Set a certain value of a constructor or method parameter. This only works if the target code was compiled with
     * `-parameters` or the @javax.inject.Named annotation is used on the parameter. This overrides any possible
     * aliases or other injection rules.
     *
     * @param executableDefinition The constructor that should receive the named parameter.
     * @param parameterName Parameter to look for.
     * @param value The exact value to pass.
     * @param  The value type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withNamedParameterValue(
        Class scope,
        Executable executableDefinition,
        String parameterName,
        T value
    ) throws MissingNamedParameterSupport {
        //noinspection unchecked
        NamedParameterSupportChecker.checkNamedParameterSupport(executableDefinition);
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues.withModified(scope, (v) -> v.with(executableDefinition, parameterName, value))
        );
    }

    /**
     * Set a certain value of a constructor or method parameter. This only works if the target code was compiled with
     * `-parameters` or the @javax.inject.Named annotation is used on the parameter. This overrides any possible
     * aliases or other injection rules.
     *
     * @param executableDefinition The constructor that should receive the named parameter.
     * @param parameterName Parameter to look for.
     * @param value The exact value to pass.
     * @param  The value type.
     *
     * @return a modified copy of this injection configuration.
     */
    public  InjectorConfiguration withNamedParameterValue(
        Executable executableDefinition,
        String parameterName,
        T value
    ) throws MissingNamedParameterSupport {
        //noinspection unchecked
        NamedParameterSupportChecker.checkNamedParameterSupport(executableDefinition);
        //noinspection unchecked
        return new InjectorConfiguration(
            scopes, definedClasses,
            factories,
            factoryClasses, sharedClasses,
            sharedInstances,
            aliases,
            collectedAliases,
            namedParameterValues.withModified((v) -> v.with(executableDefinition, parameterName, value))
        );
    }

    @Nullable
    public Object getScopedNamedParameterValue(Class scope, @Nullable Class classDefinition, String parameterName) {
        return namedParameterValues.getScope(scope).get(classDefinition, parameterName);
    }

    @Nullable
    public Object getNamedParameterValue(@Nullable Class classDefinition, String parameterName) {
        return namedParameterValues.getRootValue().get(classDefinition, parameterName);
    }


    @Nullable
    public Object getNamedParameterValue(Class scope, @Nullable Executable executable, String parameterName) {
        return namedParameterValues.getScope(scope).get(executable, parameterName);
    }

    @Nullable
    public Object getNamedParameterValue(@Nullable Executable executable, String parameterName) {
        return namedParameterValues.getRootValue().get(executable, parameterName);
    }

    public List getScopes() {
        return scopes;
    }

    private static class ImmutableScope {
        private final V rootValue;
        private final ImmutableMap scopedValues;

        private ImmutableScope(
            V rootValue
        ) {
            this.rootValue = rootValue;
            this.scopedValues = new ImmutableHashMap<>();
        }

        private ImmutableScope(
            V rootValue,
            ImmutableMap scopedValues
        ) {
            this.rootValue = rootValue;
            this.scopedValues = scopedValues;
        }

        public ImmutableScope withRootValue(V rootValue) {
            return new ImmutableScope<>(
                rootValue,
                scopedValues
            );
        }

        public ImmutableScope withScopedValue(K scope, V value) {
            return new ImmutableScope<>(
                rootValue,
                scopedValues.withPut(scope, value)
            );
        }

        public ImmutableScope withModified(Function modifier) {
            return withRootValue(
                modifier.apply(rootValue)
            );
        }

        public ImmutableScope withModified(K scope, Function modifier) throws ScopeNotFound {
            return withScopedValue(
                scope,
                modifier.apply(getScope(scope))
            );
        }

        public V getRootValue() {
            return rootValue;
        }

        public V getScope(K scope) throws ScopeNotFound {
            if (scopedValues.containsKey(scope)) {
                return scopedValues.get(scope);
            }
            throw new ScopeNotFound();
        }
    }

    private static class ImmutableMapHierarchy {
        private final ImmutableMap topLevel;
        private final ImmutableMap> classLevel;
        private final ImmutableMap> executableLevel;

        ImmutableMapHierarchy() {
            topLevel = new ImmutableHashMap<>();
            classLevel = new ImmutableHashMap<>();
            executableLevel = new ImmutableHashMap<>();
        }

        private ImmutableMapHierarchy(
            ImmutableMap topLevel,
            ImmutableMap> classLevel,
            ImmutableMap> executableLevel
        ) {
            this.topLevel = topLevel;
            this.classLevel = classLevel;
            this.executableLevel = executableLevel;
        }

        @Nullable
        V get(K key) {
            if (topLevel.containsKey(key)) {
                return topLevel.get(key);
            }
            return null;
        }
        
        ImmutableMapHierarchy with(K key, V value) {
            return new ImmutableMapHierarchy<>(
                topLevel.withPut(key, value),
                classLevel,
                executableLevel
            );
        }

        @Nullable
        V get(@Nullable Class classDefinition, K key) {
            if (classDefinition != null && classLevel.containsKey(classDefinition) && classLevel.get(classDefinition).containsKey(key)) {
                return classLevel.get(classDefinition).get(key);
            }
            return get(key);
        }

        ImmutableMapHierarchy with(Class classDefinition, K key, V value) {
            ImmutableMap> newClassLevel = classLevel.withPutIfAbsent(classDefinition, new ImmutableHashMap<>());
            newClassLevel = newClassLevel.withPut(
                classDefinition,
                newClassLevel.get(classDefinition).withPut(key, value)
            );
            return new ImmutableMapHierarchy<>(
                topLevel,
                newClassLevel,
                executableLevel
            );
        }

        @Nullable
        V get(@Nullable Executable executableDefinition, K key) {
            if (executableDefinition != null && executableLevel.containsKey(executableDefinition) && executableLevel.get(executableDefinition).containsKey(key)) {
                return executableLevel.get(executableDefinition).get(key);
            }
            return get(executableDefinition == null?null:executableDefinition.getDeclaringClass(), key);
        }

        ImmutableMapHierarchy with(Executable executableDefinition, K key, V value) {
            ImmutableMap> newExecutableLevel = executableLevel.withPutIfAbsent(executableDefinition, new ImmutableHashMap<>());
            newExecutableLevel = newExecutableLevel.withPut(
                executableDefinition,
                newExecutableLevel.get(executableDefinition).withPut(key, value)
            );
            return new ImmutableMapHierarchy<>(
                topLevel,
                classLevel,
                newExecutableLevel
            );
        }
    }

    static class ScopeNotFound extends RuntimeException {

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy