package com.github.czyzby.uedi.impl;
import java.lang.reflect.Member;
import java.util.Comparator;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IdentityMap;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.Constructor;
import com.badlogic.gdx.utils.reflect.Field;
import com.badlogic.gdx.utils.reflect.Method;
import com.github.czyzby.kiwi.util.common.Exceptions;
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.GdxSets;
import com.github.czyzby.kiwi.util.gdx.collection.pooled.PooledList;
import com.github.czyzby.uedi.Context;
import com.github.czyzby.uedi.reflection.impl.ConstructorMember;
import com.github.czyzby.uedi.reflection.impl.FieldMember;
import com.github.czyzby.uedi.reflection.impl.Modifier;
import com.github.czyzby.uedi.scanner.ClassScanner;
import com.github.czyzby.uedi.stereotype.Destructible;
import com.github.czyzby.uedi.stereotype.Factory;
import com.github.czyzby.uedi.stereotype.Initiated;
import com.github.czyzby.uedi.stereotype.Property;
import com.github.czyzby.uedi.stereotype.Provider;
import com.github.czyzby.uedi.stereotype.impl.PropertyProvider;
import com.github.czyzby.uedi.stereotype.impl.ProviderManager;
import com.github.czyzby.uedi.stereotype.impl.Providers;
import com.github.czyzby.uedi.stereotype.impl.ReflectionProvider;
import com.github.czyzby.uedi.stereotype.impl.SingletonProvider;
import com.github.czyzby.uedi.stereotype.impl.StringProvider;
/** Core implementation of context management. LibGDX-compatible: uses LibGDX reflection API wrappers rather than
* regular Java API. Not thread-safe.
*
* @author MJ */
public class DefaultContext extends AbstractContext {
/** These methods will be ignored when processing factories. */
public static final ObjectSet<String> FORBIDDEN_METHOD_NAMES = GdxSets.newSet();
private final IdentityMap<Class<?>, Provider<?>> context = GdxMaps.newIdentityMap();
private final ObjectSet<Destructible> destructibles = GdxSets.newSet();
private final StringProvider propertyProvider = getPropertyProvider();
static {
// Forbidden method names:
FORBIDDEN_METHOD_NAMES.add("toString");
FORBIDDEN_METHOD_NAMES.add("wait");
FORBIDDEN_METHOD_NAMES.add("clone");
FORBIDDEN_METHOD_NAMES.add("equals");
FORBIDDEN_METHOD_NAMES.add("finalize");
FORBIDDEN_METHOD_NAMES.add("notify");
FORBIDDEN_METHOD_NAMES.add("notifyAll");
FORBIDDEN_METHOD_NAMES.add("hashCode");
FORBIDDEN_METHOD_NAMES.add("getClass");
}
/** @param classScanner can be null, but {@link #scan(Class)} method will not work correctly. */
public DefaultContext(final ClassScanner classScanner) {
super(classScanner);
addCoreProviders();
}
/** @return default provider of {@link String} instances. */
protected StringProvider getPropertyProvider() {
return new PropertyProvider();
}
/** Registers {@link Context} (so it can be injected) and binds {@link PropertyProvider} to {@link String}
* injections. */
protected void addCoreProviders() {
context.put(String.class, propertyProvider);
context.put(Context.class, new SingletonProvider<Context>(this));
}
@Override
public void add(final Object component) {
processProvider(new SingletonProvider<Object>(component));
}
@Override
public boolean isAvailable(final Class<?> type) {
return context.containsKey(type);
}
@Override
public String getProperty(final String name) {
return propertyProvider.hasProperty(name) ? propertyProvider.getProperty(name).getValue() : null;
}
@Override
public void setProperty(final String key, final String value) {
if (propertyProvider.hasProperty(key)) {
propertyProvider.getProperty(key).setValue(value);
} else {
addProperty(new Property() {
private String property = value;
@Override
public String setValue(final String value) {
return property = value;
}
@Override
public String getValue() {
return property;
}
@Override
public String getKey() {
return key;
}
});
}
}
@Override
public void addProperty(final Property property) {
propertyProvider.addProperty(property);
}
@Override
public void addDestructible(final Destructible destructible) {
destructibles.add(destructible);
}
@Override
@SuppressWarnings("unchecked")
public <Component> Component get(final Class<Component> type, final Object forObject, final Member member) {
if (!context.containsKey(type)) {
if (isFailIfUnknownType()) {
throw new RuntimeException("Unknown component type: " + type.getName());
}
return create(type);
}
return (Component) context.get(type).provide(forObject, member);
}
@Override
@SuppressWarnings("unchecked")
public <Component> Component create(final Class<Component> type) {
final Constructor constructor = getConstructor(type);
final Object component = createObject(constructor, constructor.getParameterTypes());
initiate(component);
return (Component) component;
}
@Override
public void destroy() {
final Array<Destructible> sortedDestructibles = GdxArrays.newArray(destructibles);
sortedDestructibles.sort(new Comparator<Destructible>() {
@Override
public int compare(final Destructible o1, final Destructible o2) {
return o1.getDestructionOrder() - o2.getDestructionOrder();
}
});
destructibles.clear();
try {
for (final Destructible destructible : sortedDestructibles) {
destructible.destroy();
}
} catch (final Exception exception) {
throw new RuntimeException("Unable to destroy context.", exception);
}
}
@Override
public void destroy(final Destructible component) {
if (component != null) {
destructibles.remove(component);
try {
component.destroy();
} catch (final Exception exception) {
throw new RuntimeException("Unable to destroy: " + component, exception);
}
}
}
@Override
protected void processClasses(final Iterable<Class<?>> classes) {
try {
final Array<Initiated> componentsToInitiate = gatherComponents(gatherConstructors(classes));
componentsToInitiate.sort(new Comparator<Initiated>() {
@Override
public int compare(final Initiated o1, final Initiated o2) {
return o1.getInitiationOrder() - o2.getInitiationOrder();
}
});
for (final Initiated initiated : componentsToInitiate) {
initiated.initiate();
}
} catch (final RuntimeException exception) {
throw exception;
} catch (final Exception exception) {
throw new RuntimeException("Unable to create components.", exception);
}
}
/** @param constructors list of gathered constructors. Will be used to create the components.
* @return sorting collection of components to initiate. Should be initiated.
* @throws Exception due to reflection issues. */
protected Array<Initiated> gatherComponents(final PooledList<Constructor> constructors) throws Exception {
final Array<Initiated> componentsToInitiate = GdxArrays.newArray();
final Array<Object> components = createComponents(constructors, componentsToInitiate);
for (final Object component : components) {
injectFields(component);
}
return componentsToInitiate;
}
/** @param constructors list of gathered constructors of classes to initiate.
* @param componentsToInitiate a reference to sorting collection of components to initiate. Should be filled.
* @return list of constructed components.
* @throws Exception due to reflection issues. */
protected Array<Object> createComponents(final PooledList<Constructor> constructors,
final Array<Initiated> componentsToInitiate) throws Exception {
final Array<Object> components = GdxArrays.newArray();
for (int index = 0, iterations = getIterationsAmount(); constructors.isNotEmpty()
&& index < iterations; index++) {
for (final Constructor constructor : constructors) {
final Object component;
if (constructor.getParameterTypes().length == 0) {
component = constructor.newInstance(Providers.EMPTY_ARRAY);
} else {
final Class<?>[] parameterTypes = constructor.getParameterTypes();
if (isAnyProviderMissing(parameterTypes)) {
continue;
}
component = createObject(constructor, parameterTypes);
}
processScannedComponent(component, componentsToInitiate);
components.add(component);
constructors.remove();
}
}
if (!constructors.isEmpty()) {
if (isFailIfUnknownType()) {
final Array<String> classNames = GdxArrays.newArray();
for (final Constructor constructor : constructors) {
classNames.add(constructor.getDeclaringClass().getName());
}
throw new RuntimeException(
"Unknown or circular dependencies detected. Unable to create instances of: " + classNames);
}
for (final Constructor constructor : constructors) {
final Object component = createObject(constructor, constructor.getParameterTypes());
processScannedComponent(component, componentsToInitiate);
components.add(component);
}
}
return components;
}
/** @param constructor will be used to construct the instance.
* @param parameterTypes will be used to extract constructor parameters from the context.
* @return an instance of the class.
* @throws RuntimeException due to reflection issues. */
protected Object createObject(final Constructor constructor, final Class<?>[] parameterTypes) {
try {
if (parameterTypes.length == 0) {
return constructor.newInstance(Providers.EMPTY_ARRAY);
}
final Object[] dependencies = new Object[parameterTypes.length];
for (int index = 0, length = dependencies.length; index < length; index++) {
dependencies[index] = get(parameterTypes[index], null, new ConstructorMember(constructor));
}
return constructor.newInstance(dependencies);
} catch (final Exception exception) {
throw new RuntimeException("Unable to create an instance of: " + constructor.getDeclaringClass(),
exception);
}
}
/** @param types array of requested types.
* @return true if context currently has no provider that could supply an instance of any of the passed classes. */
protected boolean isAnyProviderMissing(final Class<?>... types) {
for (final Class<?> type : types) {
if (!context.containsKey(type)) {
return true;
}
}
return false;
}
/** @param component its interfaces will be inspected. Depending on its type, it might be initiated, scheduled for
* destruction or registered as a factory, provider or property.
* @param componentsToInitiate will be used to schedule initiations. */
protected void processScannedComponent(final Object component, final Array<Initiated> componentsToInitiate) {
processProvider(new SingletonProvider<Object>(component));
if (component instanceof Destructible) {
destructibles.add((Destructible) component);
}
if (component instanceof Factory) {
processFactory(component);
}
if (component instanceof Initiated) {
componentsToInitiate.add((Initiated) component);
}
if (component instanceof Property) {
propertyProvider.addProperty((Property) component);
}
if (component instanceof Provider<?>) {
processProvider((Provider<?>) component);
}
}
/** @param classes will have their constructors extracted. Should not contain interfaces or abstract classes.
* @return a collection of constructors allowing to create passed classes' instances. */
protected PooledList<Constructor> gatherConstructors(final Iterable<Class<?>> classes) {
final PooledList<Constructor> constructors = PooledList.newList();
for (final Class<?> componentClass : classes) {
constructors.add(getConstructor(componentClass));
}
return constructors;
}
/** @param componentClass is requested to be constructed.
* @return the first found constructor for the class. */
protected Constructor getConstructor(final Class<?> componentClass) {
final Constructor[] constructors = ClassReflection.getConstructors(componentClass);
if (constructors.length == 0) {
throw new RuntimeException("No public constructors found for component class: " + componentClass);
}
return constructors[0];
}
/** Note: this method should be invoked only with externally registered components.
*
* @param component will be initiated.
* @see #processScannedComponent(Object, Array) */
@Override
protected void processComponent(final Object component) {
injectFields(component);
if (component instanceof Initiated) {
try {
((Initiated) component).initiate();
} catch (final Exception exception) {
throw new RuntimeException("Unable to initiate component: " + component, exception);
}
}
if (component instanceof Destructible) {
destructibles.add((Destructible) component);
}
}
/** @return direct reference to component providers. */
protected IdentityMap<Class<?>, Provider<?>> getComponentProviders() {
return context;
}
/** @param component its injectable fields will be filled with values provided by the context.
* @see #isInjectable(Field, Object) */
@SuppressWarnings("unchecked")
protected void injectFields(final Object component) {
Class<?> processedClass = component.getClass();
try {
while (processedClass != null && processedClass != Object.class) {
for (final Field field : ClassReflection.getDeclaredFields(processedClass)) {
if (isInjectable(field, component)) {
field.set(component, get(field.getType(), component, new FieldMember(field)));
}
}
if (!isProcessSuperFields()) {
break;
}
processedClass = processedClass.getSuperclass();
}
} catch (final Exception exception) {
throw new RuntimeException("Unable to inject fields of component: " + component, exception);
}
}
/** @param field reflected field data.
* @param component owner of the field.
* @return true if the field is empty, accepted by the modifier filter, does not match modifier signature, not
* primitive and - if strings are ignored - not a string.
* @throws Exception due to reflection issues. */
protected boolean isInjectable(final Field field, final Object component) throws Exception {
try {
if (field.isSynthetic() || field.getType().isPrimitive()
|| isIgnoreStrings() && field.getType() == String.class) {
return false;
}
} catch (final Exception exception) {
Exceptions.ignore(exception); // GWT compatibility.
Gdx.app.debug("UEDI", "Unable to access field of component: " + component, exception);
return false;
}
final int modifier = Modifier.getModifiers(field);
if ((modifier & getFieldsIgnoreFilter()) != 0 || modifier == getFieldsIgnoreSignature()) {
return false;
}
field.setAccessible(true);
return field.get(component) == null;
}
@Override
protected void processProvider(final Provider<?> provider) {
if (!isMapSuperTypes()) {
putProvider(provider.getType(), provider);
return;
}
final PooledList<Class<?>> classesToProcess = new PooledList<Class<?>>();
classesToProcess.add(provider.getType());
while (classesToProcess.isNotEmpty()) {
final Class<?> processed = classesToProcess.removeFirst();
putProvider(processed, provider);
final Class<?> parent = processed.getSuperclass();
if (parent != null && parent != Object.class) {
classesToProcess.add(parent);
}
}
}
/** @param key provided class type.
* @param provider will be assigned as a provider of the chosen class instances. */
protected void putProvider(final Class<?> key, final Provider<?> provider) {
final Provider<?> currentProvider = context.get(key);
if (currentProvider == null) { // Unique - setting as the default provider:
context.put(key, provider);
} else if (currentProvider instanceof ProviderManager<?>) { // Already ambiguous - adding another provider:
((ProviderManager<?>) currentProvider).addProvider(provider);
} else {
@SuppressWarnings({ "rawtypes", "unchecked" }) // Ambiguous - switching to manager:
final ProviderManager<?> manager = new ProviderManager(key, this);
// Registering existing providers:
manager.addProvider(currentProvider);
manager.addProvider(provider);
// Replacing current provider with the manager:
context.put(key, manager);
}
}
@Override
public void remove(final Class<?> type) {
context.remove(type);
}
@Override
public <Type> void replace(final Class<Type> type, final Provider<? extends Type> provider) {
remove(type);
putProvider(type, provider);
}
@Override
protected void processFactory(final Object factory) {
// Registering public methods as providers:
for (final Method method : ClassReflection.getMethods(factory.getClass())) {
if (isValidFactoryMethod(method)) {
processProvider(newFactoryMethodWrapper(factory, method));
}
}
}
/** @param method cannot be synthetic, return void, have a forbidden name or have any filtered modifiers.
* @return true if the method is valid and should be converted to a provider. */
protected boolean isValidFactoryMethod(final Method method) {
final int modifiers = Modifier.getModifiers(method);
return (modifiers & getMethodsIgnoreFilter()) == 0 && modifiers != getMethodsIgnoreSignature()
&& method.getReturnType() != void.class && method.getReturnType() != Void.class
&& !FORBIDDEN_METHOD_NAMES.contains(method.getName());
}
/** @param factory owner of the method.
* @param method should be wrapped.
* @return method wrapped in a {@link Provider} implementation. */
protected Provider<?> newFactoryMethodWrapper(final Object factory, final Method method) {
return new ReflectionProvider(this, factory, method);
}
@Override
public boolean isParameterAware() {
return false;
}
@Override
public void clear(final Class<?> classTree) {
if (!isMapSuperTypes()) {
remove(classTree);
return;
}
final PooledList<Class<?>> classesToProcess = new PooledList<Class<?>>();
classesToProcess.add(classTree);
while (classesToProcess.isNotEmpty()) {
final Class<?> processed = classesToProcess.removeFirst();
remove(processed);
final Class<?> parent = processed.getSuperclass();
if (parent != null && parent != Object.class) {
classesToProcess.add(parent);
}
}
}
@Override
public void clear() {
context.clear();
addCoreProviders();
}
}