package tc.oc.commons.core.inject;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Provider;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.ProvisionException;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.internal.Annotations;
import com.google.inject.internal.Errors;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.InjectionPoint;
import com.google.inject.spi.ProviderWithDependencies;
import tc.oc.commons.core.ListUtils;
import tc.oc.commons.core.reflect.Members;
import tc.oc.commons.core.reflect.MethodHandleUtils;
import tc.oc.commons.core.reflect.MethodScanner;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.commons.core.util.ThrowingRunnable;
import tc.oc.commons.core.util.ThrowingSupplier;
import static com.google.common.base.Preconditions.checkArgument;
public class InjectableMethod<T> implements HasDependencies, Module {
private final Method method;
private final @Nullable T result;
private final MethodHandle handle;
private final Key<T> providedKey;
private final Class<? extends Annotation> scope;
private final Set<Dependency<?>> dependencies;
private List<Provider<?>> providers;
private <D> InjectableMethod(@Nullable TypeLiteral<D> targetType, @Nullable D target, Method method, @Nullable T result) {
final Errors errors = new Errors(method);
if(Members.isStatic(method)) {
checkArgument(target == null);
} else {
checkArgument(target != null);
}
targetType = targetType(targetType, target, method);
checkArgument(method.getDeclaringClass().isAssignableFrom(targetType.getRawType()));
this.method = method;
this.dependencies = ImmutableSet.copyOf(InjectionPoint.forMethod(method, targetType).getDependencies());
if(result != null) {
this.result = result;
this.providedKey = Keys.forInstance(result);
} else {
final TypeLiteral<T> returnType = (TypeLiteral<T>) targetType.getReturnType(method);
if(!Void.class.equals(returnType.getRawType())) {
final Annotation qualifier = Annotations.findBindingAnnotation(errors, method, method.getAnnotations());
this.result = null;
this.providedKey = Keys.get(returnType, qualifier);
} else {
this.result = (T) this;
this.providedKey = Keys.forInstance(this.result);
}
}
this.scope = Annotations.findScopeAnnotation(errors, method.getAnnotations());
MethodHandle handle = MethodHandleUtils.privateUnreflect(method);
if(target != null) {
handle = handle.bindTo(target);
}
this.handle = handle;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{method=" + method + "}";
}
@Override
public int hashCode() {
return method.hashCode();
}
@Override
public boolean equals(Object that) {
return this == that || (
that instanceof InjectableMethod &&
method.equals(((InjectableMethod) that).method())
);
}
public Method method() {
return method;
}
public Key<T> key() {
return providedKey;
}
public Class<? extends Annotation> scope() {
return scope;
}
public boolean hasReturnValue() {
return result == null;
}
@Override
public Set<Dependency<?>> getDependencies() {
return dependencies;
}
@Override
public void configure(Binder binder) {
binder = binder.withSource(method);
providers = ListUtils.transformedCopyOf(dependencies, binder::getProvider);
}
public Module bindingModule() {
return new KeyedManifest.Impl(method) {
@Override
public void configure() {
final Binder binder = binder().withSource(method);
if(!hasReturnValue()) {
binder.addError("Cannot bind this method as a provider because it does not return a value");
return;
}
install(InjectableMethod.this);
final ScopedBindingBuilder builder = binder.bind(providedKey).toProvider(asProvider());
if(scope != null) builder.in(scope);
}
};
}
private List<Provider<?>> providers() {
if(providers == null) {
throw new ProvisionException("Dependencies have not been injected (is the dependency module installed?)");
}
return providers;
}
public T invoke() throws Throwable {
final T t = (T) handle.invokeWithArguments(Lists.transform(providers(), Provider::get));
return result != null ? result : t;
}
public Provider<T> asProvider() {
return new ProviderWithDependencies<T>() {
@Override
public T get() {
return Injection.wrappingExceptions(asSupplier());
}
@Override
public Set<Dependency<?>> getDependencies() {
return dependencies;
}
};
}
public ThrowingSupplier<T, Throwable> asSupplier() {
//if(!hasReturnValue()) {
// throw new ProvisionException("Cannot use this method as a Supplier because it has no return value (try calling asRunnable() instead)");
//}
return this::invoke;
}
public ThrowingRunnable<Throwable> asRunnable() {
//if(hasReturnValue()) {
// throw new ProvisionException("Cannot use this method as a Runnable because it has a return value (try calling asSupplier() instead)");
//}
return this::invoke;
}
public static <D, T> InjectableMethod<T> forMethod(TypeLiteral<D> targetType, @Nullable D target, Method method, T result) {
return new InjectableMethod<>(targetType, target, method, result);
}
public static <D> InjectableMethod<?> forMethod(TypeLiteral<D> targetType, @Nullable D target, Method method) {
return new InjectableMethod<>(targetType, target, method, null);
}
public static <D> InjectableMethod<?> forDeclaredMethod(Class<D> targetType, String name, Class<?>... params) {
return forDeclaredMethod(targetType, null, name, params);
}
public static <D> InjectableMethod<?> forDeclaredMethod(D target, String name, Class<?>... params) {
return forDeclaredMethod((TypeLiteral<D>) null, target, name, params);
}
public static <D> InjectableMethod<?> forDeclaredMethod(@Nullable Class<D> targetType, @Nullable D target, String name, Class<?>... params) {
return forDeclaredMethod(targetType == null ? null : TypeLiteral.get(targetType), target, name, params);
}
public static <D> InjectableMethod<?> forDeclaredMethod(@Nullable TypeLiteral<D> targetType, @Nullable D target, String name, Class<?>... params) {
targetType = targetType(targetType, target, null);
return forMethod(targetType, target, Methods.declaredMethod(targetType.getRawType(), name, params));
}
public static <D, A extends Annotation> List<InjectableMethod<?>> forAnnotatedMethods(@Nullable TypeLiteral<D> targetType, @Nullable D target, Class<A> annotationType) {
return forInheritedMethods(targetType, target, method -> method.getAnnotation(annotationType) != null);
}
public static <D> List<InjectableMethod<?>> forInheritedMethods(@Nullable TypeLiteral<D> targetType, @Nullable D target, Predicate<? super Method> filter) {
final TypeLiteral<D> finalTargetType = targetType(targetType, target, null);
return new MethodScanner<>(finalTargetType, filter)
.methods()
.stream()
.map(method -> (InjectableMethod<?>) forMethod(finalTargetType, target, method))
.collect(Collectors.toImmutableList());
}
public static <D> List<InjectableMethod<?>> forDeclaredMethods(@Nullable TypeLiteral<D> targetType, @Nullable D target, Predicate<? super Method> filter) {
final TypeLiteral<D> finalTargetType = targetType(targetType, target, null);
return Stream
.of(targetType.getRawType().getDeclaredMethods())
.filter(filter)
.map(method -> (InjectableMethod<?>) forMethod(finalTargetType, target, method))
.collect(Collectors.toImmutableList());
}
private static <D> TypeLiteral<D> targetType(@Nullable Class<D> targetType, @Nullable D target, @Nullable Method method) {
return targetType(targetType == null ? null : TypeLiteral.get(targetType), target, method);
}
private static <D> TypeLiteral<D> targetType(@Nullable TypeLiteral<D> targetType, @Nullable D target, @Nullable Method method) {
if(targetType != null) return targetType;
if(target != null) return TypeLiteral.get((Class<D>) target.getClass());
return TypeLiteral.get((Class<D>) method.getDeclaringClass());
}
}