/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * 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.lanternpowered.server.inject.impl.reflect; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.lang3.builder.ToStringBuilder; import org.lanternpowered.server.inject.Inject; import org.lanternpowered.server.inject.Injector; import org.lanternpowered.server.inject.MethodSpec; import org.lanternpowered.server.inject.Module; import org.lanternpowered.server.inject.ParameterInfo; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nullable; final class ReflectInjector implements Injector { private final Module module; private final LoadingCache<Class<?>, TypeInjector> cache = Caffeine.newBuilder().weakKeys().build(TypeInjector::new); public ReflectInjector(Module module) { this.module = module; } @SuppressWarnings("unchecked") @Override public <T> T instantiate(Class<T> objectType) { return (T) this.getInjector(objectType).supplier.get(); } private TypeInjector getInjector(Class<?> key) { return this.cache.get(key); } @Override public void injectObjects(Object targetObject, Map<String, Object> parameters, Predicate<ParameterInfo<?>> predicate) { this.getInjector(targetObject.getClass()).injectObjects(targetObject, parameters, predicate, null); } @Override public void injectObjects(Object targetObject, Map<String, Object> parameters, Class<?> objectType) { this.injectObjects(targetObject, parameters, info -> info.getType().isAssignableFrom(objectType)); } @Override public void injectObjects(Object targetObject, Map<String, Object> parameters) { this.injectObjects(targetObject, parameters, info -> true); } @Override public <T> List<T> injectMethod(Object targetObject, MethodSpec<T> spec, Object... parameters) { return this.getInjector(targetObject.getClass()).injectMethods(targetObject, spec, parameters); } private final class TypeInjector { private final Map<MethodSpec<?>, List<Method>> methodBindings; private final List<ReflectParameterInfo<?,?>> injectObjectParameterInfos; private final Set<IgnoreEntry> ignoreMethods; private final class IgnoreEntry { private final Method method; @Nullable private final MethodSpec<?> spec; public IgnoreEntry(Method method, @Nullable MethodSpec<?> spec) { this.method = method; this.spec = spec; } @Override public boolean equals(Object other) { if (other == null || other.getClass() != this.getClass()) { return false; } IgnoreEntry o = (IgnoreEntry) other; return o.method.equals(this.method) && Objects.equals(o.spec, this.spec); } @Override public int hashCode() { return Objects.hash(this.method, this.spec); } @Override public String toString() { return new ToStringBuilder(this) .append(this.method) .append(this.spec) .build(); } } private final Class<?> superType; private final Supplier<?> supplier; @SuppressWarnings({"unchecked"}) public TypeInjector(Class<?> objectType) { final Class<?> superType = objectType.getSuperclass(); this.superType = Object.class == superType ? null : superType; final Supplier<?> supplier = module.getSupplier(objectType).orElse(null); this.supplier = supplier == null ? new ReflectSupplier(objectType) : supplier; ImmutableList.Builder<ReflectParameterInfo<?,?>> injectBuilder = ImmutableList.builder(); ImmutableSet.Builder<IgnoreEntry> ignoreBuilder = ImmutableSet.builder(); for (Field field : objectType.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(Inject.class)) { field.setAccessible(true); injectBuilder.add(new ReflectParameterInfo<Object, Field>(field, field, (Class<Object>) field.getType())); } } Map<MethodSpec<?>, ImmutableList.Builder<Method>> methodBindings = Maps.newHashMap(); for (Method method : objectType.getDeclaredMethods()) { int modifiers = method.getModifiers(); if (Modifier.isStatic(modifiers)) { continue; } if (method.isAnnotationPresent(Inject.class) && method.getParameterCount() == 1) { method.setAccessible(true); ReflectParameterInfo<Object, Method> info = new ReflectParameterInfo<Object, Method>( method.getParameters()[0], method, (Class<Object>) method.getParameterTypes()[0]); injectBuilder.add(info); this.matchSuperMethods(null, objectType, method, ignoreBuilder); } for (MethodSpec<?> binding0 : module.getMethodBindings()) { if (!Arrays.equals(method.getParameterTypes(), binding0.getParameterTypes().toArray( new Class[0]))) { continue; } List<Class<? extends Annotation>> annoTypes = Lists.newArrayList(Collections2.transform( Arrays.asList(method.getAnnotations()), anno -> anno.annotationType())); if (annoTypes.containsAll(binding0.getAnnotationTypes())) { method.setAccessible(true); methodBindings.computeIfAbsent(binding0, binding1 -> ImmutableList.builder()).add(method); this.matchSuperMethods(binding0, objectType, method, ignoreBuilder); } } } this.methodBindings = ImmutableMap.copyOf(Maps.transformValues(methodBindings, value -> value.build())); this.injectObjectParameterInfos = injectBuilder.build(); this.ignoreMethods = ignoreBuilder.build(); } private void matchSuperMethods(@Nullable MethodSpec<?> spec, Class<?> objectType, Method method, ImmutableSet.Builder<IgnoreEntry> ignoreBuilder) { if (!Modifier.isPrivate(method.getModifiers())) { Class<?> superClass = objectType; while ((superClass = superClass.getSuperclass()) != null) { try { Method superMethod = superClass.getDeclaredMethod( method.getName(), method.getParameterTypes()); int superModifiers = superMethod.getModifiers(); if (!Modifier.isPrivate(superModifiers) && !Modifier.isStatic(superModifiers)) { ignoreBuilder.add(new IgnoreEntry(superMethod, spec)); } } catch (NoSuchMethodException e) { // Ignore } } } } private final class ReflectSupplier implements Supplier<Object> { private final Constructor<?> constr; public ReflectSupplier(Class<?> type) { try { this.constr = type.getDeclaredConstructor(); } catch (Exception e) { throw new RuntimeException(e); } this.constr.setAccessible(true); } @Override public Object get() { try { return this.constr.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } } public <T> List<T> injectMethods(Object targetObject, MethodSpec<T> spec, Object[] parameters) { ImmutableList.Builder<T> results = ImmutableList.builder(); this.injectMethods(targetObject, spec, parameters, results, null); return results.build(); } @SuppressWarnings("unchecked") public <T> void injectMethods(Object targetObject, MethodSpec<T> spec, Object[] parameters, ImmutableList.Builder<T> results, Set<IgnoreEntry> ignore) { List<Method> methods = this.methodBindings.get(spec); if (methods != null) { for (Method method : methods) { if (ignore != null && ignore.contains(new IgnoreEntry(method, spec))) { continue; } try { T result = (T) method.invoke(targetObject, parameters); if (result != null) { results.add(result); } } catch (Exception e) { e.printStackTrace(); } } } if (this.superType != null) { if (ignore == null) { ignore = this.ignoreMethods; } getInjector(this.superType).injectMethods(targetObject, spec, parameters, results, ignore); } } public void injectObjects(Object targetObject, Map<String, Object> parameters, Predicate<ParameterInfo<?>> predicate, Set<IgnoreEntry> ignore) { for (ReflectParameterInfo<?,?> info : this.injectObjectParameterInfos) { if (predicate.test(info) && (ignore == null || info.accessor instanceof Field || !ignore.contains(new IgnoreEntry((Method) info.accessor, null)))) { @SuppressWarnings({"rawtypes", "unchecked"}) Object result = module.getBinding(info.spec).getProvider().get( targetObject, parameters, (ParameterInfo) info); try { if (info.accessor instanceof Field) { ((Field) info.accessor).set(targetObject, result); } else if (info.accessor instanceof Method) { ((Method) info.accessor).invoke(targetObject, result); } } catch (InvocationTargetException e) { throw new IllegalStateException(e); } catch (Exception e) { e.printStackTrace(); } } } if (this.superType != null) { if (ignore == null) { ignore = this.ignoreMethods; } getInjector(this.superType).injectObjects(targetObject, parameters, predicate, ignore); } } } }