/*
* The MIT License (MIT)
*
* Copyright (c) 2016. Diorite (by Bartłomiej Mazur (aka GotoFinal))
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.diorite.inject.impl.controller;
import java.lang.annotation.Annotation;
import java.lang.annotation.RetentionPolicy;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Lock;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.diorite.inject.AfterInject;
import org.diorite.inject.BeforeInject;
import org.diorite.inject.DelegatedQualifier;
import org.diorite.inject.Inject;
import org.diorite.inject.InjectionController;
import org.diorite.inject.InjectionException;
import org.diorite.inject.Qualifier;
import org.diorite.inject.Qualifiers;
import org.diorite.inject.Scope;
import org.diorite.inject.ShortcutInject;
import org.diorite.inject.binder.Binder;
import org.diorite.inject.binder.Provider;
import org.diorite.inject.impl.asm.AddClinitClassFileTransformer;
import org.diorite.inject.impl.data.InjectValueData;
import org.diorite.inject.impl.utils.AsmUtils;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.annotation.AnnotatedCodeElement;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.field.FieldDescription.InDefinedShape;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.description.type.TypeDescription.ForLoadedType;
import net.bytebuddy.description.type.TypeDescription.Generic;
public final class Controller extends InjectionController<AnnotatedCodeElement, TypeDescription, Generic>
{
static final TypeDescription.ForLoadedType INJECT = new TypeDescription.ForLoadedType(Inject.class);
static final TypeDescription.ForLoadedType PROVIDER = new TypeDescription.ForLoadedType(Provider.class);
static final TypeDescription.ForLoadedType SHORTCUT_INJECT = new TypeDescription.ForLoadedType(ShortcutInject.class);
static final TypeDescription.ForLoadedType DELEGATED_QUALIFIER = new TypeDescription.ForLoadedType(DelegatedQualifier.class);
static final TypeDescription.ForLoadedType QUALIFIER = new TypeDescription.ForLoadedType(Qualifier.class);
static final TypeDescription.ForLoadedType SCOPE = new TypeDescription.ForLoadedType(Scope.class);
static final TypeDescription.ForLoadedType AFTER = new TypeDescription.ForLoadedType(AfterInject.class);
static final TypeDescription.ForLoadedType BEFORE = new TypeDescription.ForLoadedType(BeforeInject.class);
final Collection<BinderValueData> bindValues = new ConcurrentLinkedQueue<>();
private final Set<Class<?>> transformed = new HashSet<>(100);
public Controller()
{
Instrumentation instrumentation = ByteBuddyAgent.getInstrumentation();
instrumentation.addTransformer(new AddClinitClassFileTransformer(this, instrumentation), false);
instrumentation.addTransformer(new TransformerOfInjectedClass(this, instrumentation), true);
}
static String fixName(TypeDescription.Generic type, String currentName)
{
if (type.asErasure().equals(PROVIDER))
{
String name = currentName.toLowerCase();
int providerIndex = name.indexOf("provider");
if (providerIndex != - 1)
{
name = currentName.substring(0, providerIndex);
}
else
{
name = currentName;
}
return name;
}
else
{
return currentName;
}
}
@Override
public void addClassData(Class<?> clazz)
{
org.diorite.inject.impl.data.ClassData<Generic> classData = this.addClassData(new ForLoadedType(clazz));
if (classData != null)
{
this.rebindSingleWithLock(classData);
}
}
@Override
protected ControllerClassData addClassData(TypeDescription typeDescription,
org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData)
{
if (! (classData instanceof ControllerClassData))
{
throw new IllegalArgumentException("Unsupported class data for this controller");
}
this.map.put(typeDescription, classData);
Lock lock = this.lock.writeLock();
try
{
lock.lock();
((ControllerClassData) classData).setIndex(this.dataList.size());
this.dataList.add(classData);
}
finally
{
lock.unlock();
}
try
{
ByteBuddyAgent.getInstrumentation().retransformClasses(AsmUtils.toClass(typeDescription));
}
catch (Exception e)
{
throw new TransformerError(e);
}
return (ControllerClassData) classData;
}
@Override
protected ControllerClassData generateMemberData(TypeDescription typeDescription)
{
boolean inject = false;
Collection<ControllerMemberData<?>> members = new LinkedList<>();
Map<String, Collection<ControllerMemberData<?>>> membersMap = new HashMap<>(2);
if (this.isInjectElement(typeDescription))
{
inject = true;
}
else
{
int index = 0;
for (InDefinedShape fieldDescription : typeDescription.getDeclaredFields())
{
if (this.isInjectElement(fieldDescription))
{
String name = fixName(fieldDescription.getType(), fieldDescription.getName());
ControllerFieldData<Object> fieldData = new ControllerFieldData<>(this, typeDescription, fieldDescription, name, index++);
members.add(fieldData);
inject = true;
Collection<ControllerMemberData<?>> memberData =
membersMap.computeIfAbsent(fieldDescription.getName().toLowerCase(), k -> new HashSet<>(2));
memberData.add(fieldData);
}
}
for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods())
{
if (this.isInjectElement(methodDescription))
{
String name;
{
String name_ = fixName(methodDescription.getReturnType(), methodDescription.getName());
if (name_.startsWith("inject"))
{
name_ = name_.substring("inject".length());
char c = Character.toLowerCase(name_.charAt(0));
name_ = c + name_.substring(1);
}
name = name_;
}
ControllerMethodData methodData = new ControllerMethodData(this, typeDescription, methodDescription, name, index++);
members.add(methodData);
inject = true;
String methodName = methodDescription.getName().toLowerCase();
Collection<ControllerMemberData<?>> memberData = membersMap.computeIfAbsent(methodName, k -> new HashSet<>(2));
membersMap.computeIfAbsent(name.toLowerCase(), k -> memberData);
memberData.add(methodData);
}
}
}
if (! inject)
{
return null;
}
ControllerClassData classData = new ControllerClassData(typeDescription, members.toArray(new ControllerMemberData<?>[members.size()]));
for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods())
{
String name = methodDescription.getName();
Annotation[] annotations = AsmUtils.getAnnotations(methodDescription, RetentionPolicy.RUNTIME);
for (Annotation annotation : annotations)
{
String[] value;
boolean after;
if (annotation instanceof BeforeInject)
{
value = ((BeforeInject) annotation).value();
after = false;
}
else if (annotation instanceof AfterInject)
{
value = ((AfterInject) annotation).value();
after = true;
}
else
{
continue;
}
if (value.length == 0)
{
if (after)
{
classData.addAfter(name);
}
else
{
classData.addBefore(name);
}
continue;
}
for (String s : value)
{
Collection<ControllerMemberData<?>> memberData = membersMap.get(s.toLowerCase());
if (memberData != null)
{
for (ControllerMemberData<?> data : memberData)
{
if (after)
{
data.addAfter(name);
}
else
{
data.addBefore(name);
}
}
}
}
}
}
return classData;
}
private boolean isAnyInjectElement(AnnotatedElement[] element)
{
for (AnnotatedElement field : element)
{
if (this.isInjectElement(field))
{
return true;
}
}
return false;
}
private void rebindSingleWithLock(org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData)
{
Lock writeLock = this.lock.writeLock();
try
{
writeLock.lock();
this.rebindSingle(classData);
}
finally
{
writeLock.unlock();
}
}
private void rebindSingle(org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData)
{
Collection<org.diorite.inject.impl.data.InjectValueData<?, Generic>> allData = new ArrayList<>(100);
for (org.diorite.inject.impl.data.MemberData<TypeDescription.ForLoadedType.Generic> fieldData : classData.getMembers())
{
allData.addAll(fieldData.getInjectValues());
}
for (org.diorite.inject.impl.data.InjectValueData<?, TypeDescription.ForLoadedType.Generic> valueData : allData)
{
this.findBinder(valueData);
}
}
void findBinder(org.diorite.inject.impl.data.InjectValueData<?, TypeDescription.ForLoadedType.Generic> valueData)
{
Iterator<BinderValueData> iterator = this.bindValues.iterator();
BinderValueData best = null;
while (iterator.hasNext())
{
BinderValueData data = iterator.next();
if (! data.isCompatible(valueData))
{
continue;
}
if ((best == null) || (best.compareTo(data) > 0))
{
best = data;
}
}
if (best == null)
{
valueData.setProvider(null);
}
else
{
valueData.setProvider(best.getProvider());
}
this.applyScopes(valueData);
}
@Override
public void rebind()
{
Lock writeLock = this.lock.writeLock();
try
{
writeLock.lock();
for (org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData : this.dataList)
{
this.rebindSingle(classData);
}
for (BinderValueData bindValue : this.bindValues)
{
Collection<? extends InjectValueData<?, ?>> injectValues = bindValue.getProvider().getInjectValues();
if (injectValues.isEmpty())
{
continue;
}
for (InjectValueData<?, ?> valueData : injectValues)
{
this.findBinder((ControllerInjectValueData<?>) valueData);
}
}
}
finally
{
writeLock.unlock();
}
}
@Override
protected ControllerClassData getClassData(Class<?> type)
{
return (ControllerClassData) this.getClassData(new ForLoadedType(type));
}
@Override
protected final <T> T getInjectedField(Object $this, int type, int field, boolean performNullCheck)
{
org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData;
Lock lock = this.lock.readLock();
try
{
lock.lock();
classData = this.dataList.get(type);
}
catch (InjectionException e)
{
throw e;
}
catch (Exception e)
{
RuntimeException runtimeException =
new RuntimeException("Can't find (missing type) to inject. (type: " + type + ", field: " + field + "), null returned instead.", e);
runtimeException.printStackTrace();
if (performNullCheck)
{
throw new InjectionException("Can't inject null value in " + $this, runtimeException);
}
return null;
}
finally
{
lock.unlock();
}
org.diorite.inject.impl.data.FieldData<T, TypeDescription.ForLoadedType.Generic> fieldData = null;
try
{
fieldData = classData.getField(field);
T result = fieldData.getValueData().tryToGet($this);
if (performNullCheck && (result == null))
{
throw new InjectionException("Can't inject null value into: " + classData.getName() + "#" + fieldData.getName());
}
return result;
}
catch (InjectionException e)
{
throw e;
}
catch (Exception e)
{
RuntimeException runtimeException =
new RuntimeException("Can't find value (missing/invalid field) to inject. (type: " + type + ", field: " + field + ", classData: " +
classData + ", fieldData: " + fieldData + "), null returned instead.", e);
runtimeException.printStackTrace();
if (performNullCheck)
{
throw new InjectionException("Can't inject null value into: " + classData.getName(), runtimeException);
}
return null;
}
}
@Override
protected final <T> T getInjectedMethod(Object $this, int type, int method, int argument, boolean performNullCheck)
{
org.diorite.inject.impl.data.ClassData<TypeDescription.ForLoadedType.Generic> classData;
Lock lock = this.lock.readLock();
try
{
lock.lock();
classData = this.dataList.get(type);
}
catch (InjectionException e)
{
throw e;
}
catch (Exception e)
{
RuntimeException runtimeException =
new RuntimeException("Can't find value (missing type) to inject. (type: " + type + ", method: " + method + ", argument: " + argument +
"), null returned instead.", e);
runtimeException.printStackTrace();
if (performNullCheck)
{
throw new InjectionException("Can't inject null value in " + $this, runtimeException);
}
return null;
}
finally
{
lock.unlock();
}
org.diorite.inject.impl.data.MethodData<TypeDescription.ForLoadedType.Generic> methodData;
try
{
methodData = classData.getMethod(method);
}
catch (InjectionException e)
{
throw e;
}
catch (Exception e)
{
RuntimeException runtimeException =
new RuntimeException("Can't find value (missing method) to inject. (type: " + type + ", method: " + method + ", argument: " + argument +
"), null returned instead.", e);
runtimeException.printStackTrace();
if (performNullCheck)
{
throw new InjectionException("Can't inject null value into: " + classData.getName(), runtimeException);
}
return null;
}
org.diorite.inject.impl.data.InjectValueData<T, TypeDescription.ForLoadedType.Generic> valueData = null;
try
{
valueData = methodData.getValueData(argument);
T result = valueData.tryToGet($this);
if (performNullCheck && (result == null))
{
throw new InjectionException("Can't inject null value into: " + classData.getName() + "#" + methodData.getName() + " param: " +
valueData.getName());
}
return result;
}
catch (InjectionException e)
{
throw e;
}
catch (Exception e)
{
RuntimeException runtimeException =
new RuntimeException("Can't find value (missing/invalid argument) to inject. (type: " + type + ", method: " + method + ", argument: " +
argument + ", classData: " + classData + ", methodData: " + methodData + ", valueData: " + valueData +
"), null returned instead.", e);
runtimeException.printStackTrace();
if (performNullCheck)
{
throw new InjectionException("Can't inject null value into: " + classData.getName() + "#" + methodData.getName(), runtimeException);
}
return null;
}
}
@Override
protected boolean isInjectElement(AnnotatedCodeElement element)
{
for (AnnotationDescription annotation : AsmUtils.getAnnotationList(element))
{
TypeDescription annotationType = annotation.getAnnotationType();
if (annotationType.equals(INJECT))
{
return true;
}
if (annotationType.getInheritedAnnotations().isAnnotationPresent(SHORTCUT_INJECT))
{
return true;
}
}
return false;
}
boolean isInjectElement(AnnotatedElement element)
{
for (Annotation annotation : element.getAnnotations())
{
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType.equals(Inject.class))
{
return true;
}
if (annotationType.isAnnotationPresent(ShortcutInject.class))
{
return true;
}
}
return false;
}
@Override
protected void extractQualifierAnnotations(ShortcutInject shortcutAnnotation, Annotation element, Consumer<Annotation> addFunc)
{
// Set<Class<? extends Annotation>> supported = Set.of(shortcutAnnotation.value());
Method[] declaredMethods = element.annotationType().getDeclaredMethods();
Map<Class<? extends Annotation>, Map<String, Entry<Method, Object>>> dataMap = new HashMap<>(declaredMethods.length);
for (Method declaredMethod : declaredMethods)
{
DelegatedQualifier delegatedQualifier = declaredMethod.getAnnotation(DelegatedQualifier.class);
if (delegatedQualifier == null)
{
continue;
}
// if (! supported.contains(delegatedQualifier.value()))
// {
// continue;
// }
Map<String, Entry<Method, Object>> map = dataMap.computeIfAbsent(delegatedQualifier.value(), k -> new HashMap<>(3));
try
{
map.put(delegatedQualifier.method(), Map.entry(declaredMethod, declaredMethod.invoke(element)));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
for (Entry<Class<? extends Annotation>, Map<String, Entry<Method, Object>>> entry : dataMap.entrySet())
{
Class<? extends Annotation> key = entry.getKey();
Map<String, Entry<Method, Object>> value = entry.getValue();
Annotation annotation;
if (value.size() == 1)
{
Entry<String, Entry<Method, Object>> next = value.entrySet().iterator().next();
if (next.getKey().isEmpty())
{
if (key.getDeclaredMethods().length == 1)
{
annotation = Qualifiers.of(key, next.getValue().getValue());
}
else
{
annotation = Qualifiers.of(key, next.getValue().getKey().getName(), next.getValue().getValue());
}
}
else
{
annotation = Qualifiers.of(key, next.getKey(), next.getValue().getValue());
}
}
else
{
Map<String, Object> data = new HashMap<>(value.size());
for (Entry<String, Entry<Method, Object>> entryEntry : value.entrySet())
{
data.put(entryEntry.getKey(), entryEntry.getValue().getValue());
}
annotation = Qualifiers.of(key, data);
}
addFunc.accept(annotation);
}
}
@Override
protected Map<Class<? extends Annotation>, ? extends Annotation> extractRawQualifierAnnotations(AnnotatedCodeElement element)
{
Annotation[] annotations = AsmUtils.getAnnotations(element, RetentionPolicy.RUNTIME);
Map<Class<? extends Annotation>, Annotation> resultMap = new HashMap<>(annotations.length + 1);
for (Annotation annotation : annotations)
{
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType.isAnnotationPresent(Qualifier.class))
{
if (resultMap.containsKey(annotationType))
{
throw new IllegalStateException("Duplicated qualifier! Found: " + annotation + ", others: " + resultMap);
}
resultMap.put(annotationType, annotation);
}
if (annotationType.isAnnotationPresent(ShortcutInject.class))
{
this.extractQualifierAnnotations(annotationType.getAnnotation(ShortcutInject.class), annotation, r ->
{
Class<? extends Annotation> type = r.annotationType();
if (resultMap.containsKey(type))
{
throw new IllegalStateException("Duplicated qualifier! Found: " + r + ", others: " + resultMap);
}
resultMap.put(type, r);
});
}
}
return resultMap;
}
@Override
protected Map<Class<? extends Annotation>, ? extends Annotation> extractRawScopeAnnotations(AnnotatedCodeElement element)
{
Annotation[] annotations = AsmUtils.getAnnotations(element, RetentionPolicy.RUNTIME);
Map<Class<? extends Annotation>, Annotation> resultMap = new HashMap<>(annotations.length + 1);
for (Annotation annotation : annotations)
{
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType.isAnnotationPresent(Scope.class))
{
if (resultMap.containsKey(annotationType))
{
throw new IllegalStateException("Duplicated scope! Found: " + annotation + ", others: " + resultMap);
}
resultMap.put(annotationType, annotation);
}
}
return resultMap;
}
@Override
public <T> Binder<T> bindToClass(Predicate<Class<?>> typePredicate)
{
return new BinderSimpleInstance<>(this, type ->
{
try
{
return typePredicate.test(Class.forName(type.asErasure().getActualName()));
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
return false;
}
});
}
public <T> Binder<T> bindToType(Predicate<Generic> typePredicate)
{
return new BinderSimpleInstance<>(this, typePredicate);
}
@Override
protected Map<Class<? extends Annotation>, ? extends Annotation> transformAll(TypeDescription classType, String name,
AnnotatedCodeElement member,
Map<Class<? extends Annotation>, ? extends Annotation> raw)
{
Map<Class<? extends Annotation>, Annotation> scopeAnnotations = new HashMap<>(raw.size());
for (Entry<Class<? extends Annotation>, ? extends Annotation> entry : raw.entrySet())
{
Annotation value = this.transform(entry.getValue(), new ControllerMemberDataBuilderImpl<>(classType, name, member, raw));
scopeAnnotations.put(entry.getKey(), value);
}
return scopeAnnotations;
}
<T, B extends AnnotatedCodeElement & NamedElement.WithRuntimeName> ControllerInjectValueData<T> createValue
(int index, TypeDescription classType, TypeDescription.ForLoadedType.Generic type, B member, String name,
Map<Class<? extends Annotation>, ? extends Annotation> parentRawScopeAnnotations,
Map<Class<? extends Annotation>, ? extends Annotation> parentRawQualifierAnnotations)
{
Map<Class<? extends Annotation>, ? extends Annotation> scopeAnnotations;
{
Map<Class<? extends Annotation>, ? extends Annotation> memberRawScopeAnnotations = this.extractRawScopeAnnotations(member);
Map<Class<? extends Annotation>, Annotation> rawScopeAnnotations = new HashMap<>(parentRawScopeAnnotations);
rawScopeAnnotations.putAll(memberRawScopeAnnotations);
scopeAnnotations = this.transformAll(classType, name, member, rawScopeAnnotations);
}
Map<Class<? extends Annotation>, ? extends Annotation> qualifierAnnotations;
{
Map<Class<? extends Annotation>, ? extends Annotation> memberRawQualifierAnnotations = this.extractRawQualifierAnnotations(member);
Map<Class<? extends Annotation>, Annotation> rawQualifierAnnotations = new HashMap<>(parentRawQualifierAnnotations);
rawQualifierAnnotations.putAll(memberRawQualifierAnnotations);
qualifierAnnotations = this.transformAll(classType, name, member, rawQualifierAnnotations);
}
return new ControllerInjectValueData<>(index, name, type, scopeAnnotations, qualifierAnnotations);
}
}