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

io.datakernel.di.module.ModuleBuilderImpl Maven / Gradle / Ivy

package io.datakernel.di.module;

import io.datakernel.di.annotation.KeySetAnnotation;
import io.datakernel.di.core.*;
import io.datakernel.di.util.LocationInfo;
import io.datakernel.di.util.Trie;
import io.datakernel.di.util.Types;
import io.datakernel.di.util.Utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import static io.datakernel.di.core.Scope.UNSCOPED;
import static io.datakernel.di.impl.CompiledBinding.missingOptionalBinding;
import static io.datakernel.di.util.ReflectionUtils.scanClassHierarchy;
import static io.datakernel.di.util.Utils.*;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;

@SuppressWarnings("UnusedReturnValue")
final class ModuleBuilderImpl implements ModuleBuilderBinder {
	private static final Binding TO_BE_GENERATED = new Binding<>(emptySet(), (compiledBindings, threadsafe, scope, index) -> missingOptionalBinding());

	private final List bindingDescs = new ArrayList<>();

	private Trie, Set>>> bindings = Trie.leaf(new HashMap<>());
	private Map>> bindingTransformers = new HashMap<>();
	private Map, Set>> bindingGenerators = new HashMap<>();
	private Map, Multibinder> multibinders = new HashMap<>();

	private final AtomicBoolean configured = new AtomicBoolean();

	@Nullable
	private volatile BindingDesc current = null;

	@Nullable
	private final StackTraceElement location;

	ModuleBuilderImpl() {
		// builder module is (and should be) never instantiated directly,
		// only by some factory methods (mainly the Module.create() ofc)
		StackTraceElement[] trace = Thread.currentThread().getStackTrace();
		location = trace.length >= 3 ? trace[3] : null;
	}

	private void completeCurrent() {
		BindingDesc prev = current;
		if (prev != null) {
			bindingDescs.add(prev);
			current = null;
		}
	}

	@Override
	@SuppressWarnings("unchecked")
	public  ModuleBuilderBinder bind(@NotNull Key key) {
		checkState(!configured.get(), "Cannot bind after the module builder was used as a module");
		completeCurrent();
		current = new BindingDesc(key, TO_BE_GENERATED, UNSCOPED, false);
		return (ModuleBuilderBinder) this;
	}

	private BindingDesc ensureCurrent() {
		checkState(!configured.get(), "Cannot use the module builder DSL after the module was used");
		BindingDesc desc = current;
		checkState(desc != null, "Cannot configure binding before bind(...) call");
		return desc;
	}

	@Override
	public ModuleBuilderBinder named(@NotNull Name name) {
		BindingDesc desc = ensureCurrent();
		Key key = desc.getKey();
		if (key.getName() != null) {
			throw new IllegalStateException("Already annotated with " + key.getName().getDisplayString());
		}
		desc.setKey(key.named(name));
		return this;
	}

	@Override
	public ModuleBuilderBinder in(@NotNull Scope[] scope) {
		BindingDesc desc = ensureCurrent();
		if (desc.getScope().length != 0) {
			throw new IllegalStateException("Already bound to scope " + getScopeDisplayString(desc.getScope()));
		}
		desc.setScope(scope);
		return this;
	}

	@Override
	public ModuleBuilderBinder in(@NotNull Scope scope, @NotNull Scope... scopes) {
		Scope[] joined = new Scope[scopes.length + 1];
		joined[0] = scope;
		System.arraycopy(scopes, 0, joined, 1, scopes.length);
		return in(joined);
	}

	@SuppressWarnings("unchecked")
	@Override
	public final ModuleBuilderBinder in(@NotNull Class annotationClass, @NotNull Class... annotationClasses) {
		return in(Stream.concat(Stream.of(annotationClass), Arrays.stream((Class[]) annotationClasses)).map(Scope::of).toArray(Scope[]::new));
	}

	@Override
	public ModuleBuilderBinder to(@NotNull Binding binding) {
		BindingDesc desc = ensureCurrent();
		checkState(desc.getBinding() == TO_BE_GENERATED, "Already mapped to a binding");
		if (binding.getLocation() == null) {
			binding.at(LocationInfo.from(this));
		}
		desc.setBinding(binding);
		return this;
	}

	@Override
	public ModuleBuilderBinder as(@NotNull Name name) {
		checkArgument(name.isMarkedBy(KeySetAnnotation.class), "Should be a key set name");
		checkState(!configured.get(), "Cannot use the module builder DSL after the module was used");

		Key>> setKey = new Key>>(name) {};

		BindingDesc desc = ensureCurrent();

		// binding constructor closes over the desc because the key could be modified after the .as() call
		bindingDescs.add(new BindingDesc(setKey, Binding.to(() -> singleton(desc.getKey())), UNSCOPED, false));

		multibinders.put(setKey, Multibinder.toSet());
		return this;
	}

	@Override
	public ModuleBuilderBinder export() {
		BindingDesc current = ensureCurrent();
		checkState(!current.isExported(), "Binding was already exported");
		current.setExported();
		return this;
	}

	@Override
	public ModuleBuilder scan(@NotNull Class moduleClass, @Nullable Object module) {
		checkState(!configured.get(), "Cannot add declarative bindings after the module builder was used as a module");
		return install(scanClassHierarchy(moduleClass, module).values());
	}

	@Override
	public ModuleBuilder install(Collection modules) {
		checkState(!configured.get(), "Cannot install modules after the module builder was used as a module");
		completeCurrent();
		for (Module module : modules) {
			bindings.addAll(module.getBindings(), multimapMerger());
			combineMultimap(bindingTransformers, module.getBindingTransformers());
			combineMultimap(bindingGenerators, module.getBindingGenerators());
			mergeMultibinders(multibinders, module.getMultibinders());
		}
		return this;
	}

	@Override
	public  ModuleBuilder bindIntoSet(Key setOf, Binding binding) {
		checkState(!configured.get(), "Cannot install modules after the module builder was used as a module");

		completeCurrent();

		Key> set = Key.ofType(Types.parameterized(Set.class, setOf.getType()), setOf.getName());

		bindingDescs.add(new BindingDesc(set, binding.mapInstance(Collections::singleton), UNSCOPED, false));

		multibinders.put(set, Multibinder.toSet());
		return this;
	}

	@Override
	public  ModuleBuilder transform(int priority, BindingTransformer bindingTransformer) {
		checkState(!configured.get(), "Cannot add transformers after the module builder was used as a module");
		completeCurrent();

		bindingTransformers.computeIfAbsent(priority, $ -> new HashSet<>()).add(bindingTransformer);
		return this;
	}

	@Override
	public  ModuleBuilder generate(Class pattern, BindingGenerator bindingGenerator) {
		checkState(!configured.get(), "Cannot add generators after the module builder was used as a module");
		completeCurrent();

		bindingGenerators.computeIfAbsent(pattern, $ -> new HashSet<>()).add(bindingGenerator);
		return this;
	}

	@Override
	public  ModuleBuilder multibind(Key key, Multibinder multibinder) {
		checkState(!configured.get(), "Cannot add multibinders after the module builder was used as a module");
		completeCurrent();

		multibinders.put(key, multibinder);
		return this;
	}

	private void finish() {
		if (!configured.compareAndSet(false, true)) {
			return;
		}
		completeCurrent(); // finish the last binding

		bindingDescs.forEach(b -> {
			Set> bindingSet = this.bindings.computeIfAbsent(b.getScope(), $ -> new HashMap<>())
					.get()
					.computeIfAbsent(b.getKey(), $ -> new HashSet<>());
			Binding binding = b.getBinding();
			if (binding != TO_BE_GENERATED) {
				bindingSet.add(binding);
			}
		});

		Set> exportedKeys = bindingDescs.stream()
				.filter(BindingDesc::isExported)
				.map(BindingDesc::getKey)
				.collect(toSet());

		if (!exportedKeys.isEmpty()) {

			// key sets are always exported
			bindings.dfs(bindings -> bindings.keySet().stream().filter(Utils::isKeySet).forEach(exportedKeys::add));

			Module exported = Modules.export(this, exportedKeys);
			// it would not recurse because we have the `finished` flag
			// and it's ok to reassign all of that below in the last moment

			bindings = exported.getBindings();
			bindingTransformers = exported.getBindingTransformers();
			bindingGenerators = exported.getBindingGenerators();
			multibinders = exported.getMultibinders();
		}
	}

	@Override
	public final Trie, Set>>> getBindings() {
		finish();
		return bindings;
	}

	@Override
	public final Map>> getBindingTransformers() {
		finish();
		return bindingTransformers;
	}

	@Override
	public final Map, Set>> getBindingGenerators() {
		finish();
		return bindingGenerators;
	}

	@Override
	public final Map, Multibinder> getMultibinders() {
		finish();
		return multibinders;
	}

	@Override
	public String toString() {
		return "BuilderModule(at " + (location != null ? location : "") + ')';
	}
}