/** * * Copyright (c) 2006-2017, 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 = LoggerManager.getLogger(InjectorBuilderImpl.class); private final ClassLoader classLoader; private final Map<String, List<Class<?>>> injectables; private final List<ExecutionBuilder<?>> executions; private final Map<String, String> overriddenParams; private Path configFileLocation; InjectorBuilderImpl() { this(defaultClassLoader(), Collections.emptySet()); } InjectorBuilderImpl(ClassLoader classLoader) { this(classLoader, Collections.emptySet()); } InjectorBuilderImpl(Set<Class<?>> injectables) { this(defaultClassLoader(), injectables); } InjectorBuilderImpl(ClassLoader classLoader, Set<Class<?>> 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<? extends InjectBundle> 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 <T> InjectorBuilder before(ExecutionBuilder<T> 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, configFile); overriddenParams.forEach(properties::setProperty); final Set<Class<?>> injectablesSet = unmodifiableSet( injectables.values().stream() .flatMap(List::stream) .collect(toCollection(() -> new LinkedHashSet<>())) ); final DependencyGraph graph = DependencyGraphImpl.create(injectablesSet); final LinkedList<Object> instances = new LinkedList<>(); LOGGER.debug("Creating " + injectablesSet.size() + " injectable instances."); LOGGER.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.getLevel().isEqualOrLowerThan(Level.DEBUG)) { LOGGER.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::debug); LOGGER.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> T apply(Class<T> 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<DependencyNode> 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.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<Object> casted = (Execution<Object>) exec; return casted; }) .forEach(exec -> { // We might want to log exactly which steps we // have completed. if (LOGGER.getLevel() .isEqualOrLowerThan(Level.DEBUG)) { LOGGER.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.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.debug(horizontalLine()); LOGGER.debug( "| %-79s |", "All " + instances.size() + " components have been configured!" ); LOGGER.debug(horizontalLine()); return injector; } private void appendInjectable(String key, Class<?> clazz, boolean overwrite) { final List<Class<?>> 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(); } }