/* * 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); } }