package com.github.czyzby.autumn.context;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.github.czyzby.autumn.provider.DependencyProvider;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.kiwi.util.gdx.collection.lazy.LazyObjectMap;
import com.github.czyzby.kiwi.util.gdx.reflection.Reflection;
/** Temporary object that holds references to components. Should be cleared and garbage collected - do not keep
* reference to this object after context creation, unless you need constant access by type.
*
* @author MJ */
public class Context {
private final ObjectMap<Class<?>, Array<Object>> components = LazyObjectMap.newMapOfArrays();
private final ObjectMap<Class<?>, Array<DependencyProvider<?>>> providers = LazyObjectMap.newMapOfArrays();
private boolean createMissingDependencies = true;
private boolean clear = true;
/** Creates a new empty context. */
public Context() {
map(this);
}
/** Static factory method for {@code ContextInitializer}
*
* @return new instance of {@code ContextInitializer} with default annotation procesors. */
public static ContextInitializer builder() {
return new ContextInitializer();
}
/** @param clear if false, context will not be cleared upon {@link #clear()} call and it will be usable during
* application's lifecycle. Normally, context is cleared and garbage-collected after initiation, but if
* you need constant access to components mapped by their types, set this to false. */
public void setClear(final boolean clear) {
this.clear = clear;
}
/** @param createMissingDependencies if true and an instance of unknown class is required (not in context and no
* provider), it will be created using reflection by a no-arg constructor. Defaults to true. */
public void setCreateMissingDependencies(final boolean createMissingDependencies) {
this.createMissingDependencies = createMissingDependencies;
}
/** @return if true and an instance of unknown class is required (not in context and no provider), it will be
* created using reflection by a no-arg constructor. Defaults to true. */
public boolean isCreatingMissingDependencies() {
return createMissingDependencies;
}
/** @param type superclass or interface of the component.
* @param component will be injectable through the selected class. */
public void add(final Class<?> type, final Object component) {
components.get(type).add(component);
}
/** @param component will be mapped to its class tree. Note that mapping by interfaces is NOT supported, as
* interfaces are not available on GWT. */
public void map(final Object component) {
Class<?> componentClass = component.getClass();
while (componentClass != null && !componentClass.equals(Object.class)) {
components.get(componentClass).add(component);
componentClass = componentClass.getSuperclass();
}
}
/** @param componentClass class of the component.
* @return true if at least one component is mapped to the passed class. */
public boolean isPresent(final Class<?> componentClass) {
return GdxArrays.isNotEmpty(components.get(componentClass));
}
/** @param componentClass class of the component.
* @return a single component mapped to the class.
* @throws GdxRuntimeException if no components or multiple components mapped to the selected class.
* @see #isPresent(Class) */
public Object getComponent(final Class<?> componentClass) {
final Array<Object> componentsForClass = components.get(componentClass);
if (GdxArrays.isEmpty(componentsForClass)) {
throw new GdxRuntimeException("Component for class: " + componentClass + " not available in context.");
} else if (GdxArrays.sizeOf(componentsForClass) > 1) {
throw new GdxRuntimeException("Multiple components mapped to: " + componentClass + ". Be more specific.");
}
return componentsForClass.first();
}
/** @param componentClass class of the components.
* @return all components currently mapped to the selected class. Might be empty. */
public Array<Object> getAll(final Class<?> componentClass) {
return components.get(componentClass);
}
/** @param dependencyClass required class.
* @return true if there is a provider present for the selected class. */
public boolean isProviderPresentFor(final Class<?> dependencyClass) {
return providers.containsKey(dependencyClass);
}
/** @param dependencyClass required class.
* @return an instance of the class. If there is a component of this class in the context, component will be
* returned. If there is a provider that provides instances of this class, provider's result will be
* returned. If {@link #setCreateMissingDependencies(boolean)} is set to true, a new instance of the class
* will be created with no-arg constructor. Otherwise, an exception is thrown.
* @throws GdxRuntimeException if there are multiple components or providers mapped to the same class, if unable to
* create a new instance with no-arg constructor or if unable to provide an instance at all.
* @param <Type> type of the provided object. */
@SuppressWarnings("unchecked")
public <Type> Type provide(final Class<Type> dependencyClass) {
if (isPresent(dependencyClass)) {
// Components are mapped by their type, safe to cast.
return (Type) getComponent(dependencyClass);
} else if (isProviderPresentFor(dependencyClass)) {
return getProvider(dependencyClass).provide();
} else if (createMissingDependencies) {
return Reflection.newInstance(dependencyClass);
}
throw new GdxRuntimeException("Unable to provide an instance of: " + dependencyClass
+ ". Not available in context and no provider selected.");
}
/** @param provider registers provider for the class true of the provided type. */
public void addProvider(final DependencyProvider<?> provider) {
Class<?> depedencyClass = provider.getDependencyType();
while (depedencyClass != null && !depedencyClass.equals(Object.class)) {
providers.get(depedencyClass).add(provider);
depedencyClass = depedencyClass.getSuperclass();
}
}
/** @param dependencyClass requested class.
* @return provider that provides instances of the requested class.
* @param <Type> type of requested class objects provided by the returned provider.
* @see #isPresent(Class)
* @throws GdxRuntimeException if unable to select 1 provider for class. */
@SuppressWarnings("unchecked")
public <Type> DependencyProvider<Type> getProvider(final Class<Type> dependencyClass) {
final Array<DependencyProvider<?>> providersForClass = providers.get(dependencyClass);
if (GdxArrays.isEmpty(providersForClass)) {
throw new GdxRuntimeException(
"Unable to return provider of: " + dependencyClass + ". No providers available for this class.");
} else if (GdxArrays.sizeOf(providersForClass) != 1) {
throw new GdxRuntimeException("Unable to return a provider of: " + dependencyClass
+ ". Multiple providers mapped to the same type. Pass more specific class.");
}
return (DependencyProvider<Type>) providersForClass.first();
}
/** Clears context meta-data. Context object might become unusable after this call.
*
* @see #setClear(boolean) */
public void clear() {
if (clear) {
GdxMaps.clearAll(components);
}
}
}