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

com.speedment.common.injector.internal.InjectorBuilderImpl Maven / Gradle / Ivy

Go to download

A Speedment bundle that shades all dependencies into one jar. This is useful when deploying an application on a server.

There is a newer version: 3.1.18
Show newest version
/**
 *
 * Copyright (c) 2006-2019, Speedment, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); You may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.speedment.common.injector.internal;

import com.speedment.common.injector.InjectBundle;
import com.speedment.common.injector.Injector;
import com.speedment.common.injector.InjectorBuilder;
import com.speedment.common.injector.State;
import com.speedment.common.injector.annotation.Config;
import com.speedment.common.injector.annotation.Inject;
import com.speedment.common.injector.annotation.InjectKey;
import com.speedment.common.injector.annotation.WithState;
import com.speedment.common.injector.dependency.DependencyGraph;
import com.speedment.common.injector.dependency.DependencyNode;
import com.speedment.common.injector.exception.NoDefaultConstructorException;
import com.speedment.common.injector.exception.NotInjectableException;
import com.speedment.common.injector.execution.Execution;
import com.speedment.common.injector.execution.Execution.ClassMapper;
import com.speedment.common.injector.execution.ExecutionBuilder;
import com.speedment.common.injector.internal.dependency.DependencyGraphImpl;
import static com.speedment.common.injector.internal.util.InjectorUtil.findIn;
import static com.speedment.common.injector.internal.util.PrintUtil.horizontalLine;
import static com.speedment.common.injector.internal.util.PrintUtil.limit;
import static com.speedment.common.injector.internal.util.PropertiesUtil.loadProperties;
import static com.speedment.common.injector.internal.util.ReflectionUtil.newInstance;
import static com.speedment.common.injector.internal.util.ReflectionUtil.traverseAncestors;
import static com.speedment.common.injector.internal.util.ReflectionUtil.traverseFields;
import com.speedment.common.logger.Level;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toSet;

/**
 * Default implementation of the {@link InjectorBuilder}-interface.
 * 
 * @author Emil Forslund
 * @since  1.2.0
 */
public final class InjectorBuilderImpl implements InjectorBuilder {
    
    public final static Logger LOGGER_INSTANCE =
        LoggerManager.getLogger(InjectorBuilderImpl.class);

    private final ClassLoader classLoader;
    private final Map>> injectables;
    private final List> executions;
    private final Map overriddenParams;
    private Path configFileLocation;

    InjectorBuilderImpl() {
        this(defaultClassLoader(), Collections.emptySet());
    }

    InjectorBuilderImpl(ClassLoader classLoader) {
        this(classLoader, Collections.emptySet());
    }

    InjectorBuilderImpl(Set> injectables) {
        this(defaultClassLoader(), injectables);
    }

    InjectorBuilderImpl(ClassLoader classLoader, Set> injectables) {
        requireNonNull(injectables);

        this.classLoader        = requireNonNull(classLoader);
        this.injectables        = new LinkedHashMap<>();
        this.executions         = new LinkedList<>();
        this.overriddenParams   = new HashMap<>();
        this.configFileLocation = Paths.get("settings.properties");

        injectables.forEach(this::withComponent);
    }

    @Override
    public InjectorBuilder withComponent(Class injectableType) {
        requireNonNull(injectableType);

        // Store the injectable under every superclass in the map, as well
        // as under every inherited InjectorKey value.
        traverseAncestors(injectableType)

            // only include classes that has an ancestor with the 
            // InjectorKey-annotation, or that are the original class.
            .filter(c -> c == injectableType || traverseAncestors(c)
                .anyMatch(c2 -> c2.isAnnotationPresent(InjectKey.class))
            )

            .forEachOrdered(c -> {
                // Store it under the class name itself
                appendInjectable(c.getName(), injectableType, true);

                // Include InjectorKey value
                if (c.isAnnotationPresent(InjectKey.class)) {
                    final InjectKey key = c.getAnnotation(InjectKey.class);
                    appendInjectable(
                        key.value().getName(), 
                        injectableType, 
                        key.overwrite()
                    );
                }
            });

        return this;
    }

    @Override
    public InjectorBuilder withBundle(Class bundleClass) {
        try {
            final InjectBundle bundle = bundleClass.newInstance();
            bundle.injectables().forEach(this::withComponent);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new NoDefaultConstructorException(e);
        }
        return this;
    }

    @Override
    public InjectorBuilder withConfigFileLocation(Path configFile) {
        this.configFileLocation = requireNonNull(configFile);
        return this;
    }

    @Override
    public InjectorBuilder withParam(String name, String value) {
        overriddenParams.put(name, value);
        return this;
    }

    @Override
    public  InjectorBuilder before(ExecutionBuilder executionBuilder) {
        executions.add(requireNonNull(executionBuilder));
        return this;
    }

    @Override
    public Injector build() 
    throws InstantiationException, NoDefaultConstructorException {

        // Load settings
        final File configFile = configFileLocation.toFile();
        final Properties properties = loadProperties(LOGGER_INSTANCE, configFile);
        overriddenParams.forEach(properties::setProperty);

        final Set> injectablesSet = unmodifiableSet(
            injectables.values().stream()
                .flatMap(List::stream)
                .collect(toCollection(() -> new LinkedHashSet<>()))
        );

        final DependencyGraph graph = 
            DependencyGraphImpl.create(injectablesSet);

        final LinkedList instances = new LinkedList<>();

        LOGGER_INSTANCE.debug("Creating " + injectablesSet.size() +
            " injectable instances.");

        LOGGER_INSTANCE.debug(horizontalLine());

        // Create an instance of every injectable type
        for (final Class injectable : injectablesSet) {

            // If we are currently debugging, print out every created
            // instance and which configuration options are available for
            // it.
            if (LOGGER_INSTANCE.getLevel().isEqualOrLowerThan(Level.DEBUG)) {
                LOGGER_INSTANCE.debug("| %-71s CREATED |",
                    limit(injectable.getSimpleName(), 71)
                );

                traverseFields(injectable)
                    .filter(f -> f.isAnnotationPresent(Config.class))
                    .map(f -> f.getAnnotation(Config.class))
                    .map(a -> String.format(
                        "|     %-48s %26s |", 
                        limit(a.name(), 48),
                        limit(properties.containsKey(a.name())
                            ? properties.get(a.name()).toString()
                            : a.value(), 26
                        )
                    ))
                    .forEachOrdered(LOGGER_INSTANCE::debug);

                LOGGER_INSTANCE.debug(horizontalLine());
            }

            final Object instance = newInstance(injectable, properties);
            instances.addFirst(instance);
        }

        // Build the Injector
        final Injector injector = new InjectorImpl(
            injectablesSet,
            unmodifiableList(instances),
            properties,
            classLoader,
            graph,
            this
        );
        
        // Create ClassMapper
        final ClassMapper classMapper = new ClassMapper() {
            @Override
            public  T apply(Class type) 
                throws NotInjectableException {
                return findIn(
                    type, 
                    injector, 
                    instances,
                    true // Required = true
                );
            }
        };

        // Set the auto-injected fields
        instances.forEach(instance -> traverseFields(instance.getClass())
            .filter(f -> f.isAnnotationPresent(Inject.class))
            .distinct()
            .forEachOrdered(field -> {
                final Object value;

                if (Inject.class.isAssignableFrom(field.getType())) {
                    value = injector;
                } else {
                    value = findIn(
                        field.getType(),
                        injector,
                        instances, 
                        field.getAnnotation(WithState.class) != null
                    );
                }

                field.setAccessible(true);

                try {
                    field.set(instance, value);
                } catch (final IllegalAccessException ex) {
                    throw new RuntimeException(
                        "Could not access field '" + field.getName()
                            + "' in class '" + value.getClass().getName()
                            + "' of type '" + field.getType()
                            + "'.", ex
                    );
                }
            })
        );
        
        // Build explicit executions and add them to the graph
        executions.stream()
            .map(builder -> builder.build(graph))
            .forEachOrdered(execution -> {
                final DependencyNode node = graph.get(execution.getType());
                node.getExecutions().add(execution);
            });

        final AtomicBoolean hasAnythingChanged = new AtomicBoolean();
        final AtomicInteger nextState = new AtomicInteger(0);

        // Loop until all nodes have been started.
        Set unfinished;

        // Go through every state up and including STARTED.
        while (nextState.get() <= State.STARTED.ordinal()) {

            // Get a set of the nodes that has not yet reached that state,
            // and operate upon it until it is empty
            while (!(unfinished = graph.nodes()
                .filter(n -> n.getCurrentState().ordinal() < nextState.get())
                .collect(toSet())).isEmpty()) {

                hasAnythingChanged.set(false);

                unfinished.forEach(n -> {
                    // Determine the next state of this node.
                    final State state = State.values()[
                        n.getCurrentState().ordinal() + 1
                    ];

                    // Check if all its dependencies have been satisfied.
                    if (n.canBe(state)) {

                        LOGGER_INSTANCE.debug(horizontalLine());

                        // Retreive the instance for that node
                        final Object instance = findIn(
                            n.getRepresentedType(), 
                            injector, 
                            instances, 
                            true
                        );

                        // Execute all the executions for the next step.
                        n.getExecutions().stream()
                            .filter(e -> e.getState() == state)
                            .map(exec -> {
                                @SuppressWarnings("unchecked")
                                final Execution casted = 
                                    (Execution) exec;
                                return casted;
                            })
                            .forEach(exec -> {
                                
                                // We might want to log exactly which steps we
                                // have completed.
                                if (LOGGER_INSTANCE.getLevel()
                                    .isEqualOrLowerThan(Level.DEBUG)) {
                                    
                                    LOGGER_INSTANCE.debug(
                                        "| -> %-76s |", 
                                        limit(exec.toString(), 76)
                                    );
                                }
                                
                                try {
                                    exec.invoke(instance, classMapper);
                                } catch (final IllegalAccessException 
                                             | IllegalArgumentException 
                                             | InvocationTargetException ex) {

                                    throw new RuntimeException(ex);
                                }
                            });

                        // Update its state to the new state.
                        n.setState(state);
                        hasAnythingChanged.set(true);

                        LOGGER_INSTANCE.debug(
                            "| %-66s %12s |",
                            limit(n.getRepresentedType().getSimpleName(), 66),
                            limit(state.name(), 12)
                        );
                    }
                });

                // The set was not empty when we entered the 'while' clause, 
                // and yet nothing has changed. This means that we are stuck
                // in an infinite loop.
                if (!hasAnythingChanged.get()) {
                    throw new IllegalStateException(
                        "Injector appears to be stuck in an infinite loop."
                    );
                }
            }

            // Every node has reached the desired state. 
            // Begin working with the next state.
            nextState.incrementAndGet();
        }

        LOGGER_INSTANCE.debug(horizontalLine());
        LOGGER_INSTANCE.debug(
            "| %-79s |",
            "All " + instances.size() + " components have been configured!"
        );
        LOGGER_INSTANCE.debug(horizontalLine());

        return injector;
    }

    private void appendInjectable(String key, Class clazz, boolean overwrite) {
        final List> list = Optional.ofNullable(
            injectables.remove(key)
        ).orElseGet(LinkedList::new);

        if (overwrite) {
            list.clear();
        }

        list.add(clazz);
        injectables.put(key, list);
    }

    private static ClassLoader defaultClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }
}