package tc.oc.pgm.match; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; import javax.inject.Inject; import com.google.common.base.Joiner; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableSet; import java.time.Duration; import tc.oc.commons.core.exception.ExceptionHandler; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.reflect.Methods; import tc.oc.commons.core.scheduler.Scheduler; import tc.oc.commons.core.scheduler.SchedulerBackend; import tc.oc.commons.core.scheduler.Task; import tc.oc.commons.core.stream.Collectors; import tc.oc.time.Time; import tc.oc.commons.core.util.CacheUtils; import tc.oc.commons.core.util.ThrowingRunnable; /** * A scheduler that is active for the duration of a {@link Match}. */ public class MatchScheduler extends Scheduler { private final Match match; private final MatchScope scope; private final Map<Object, Set<Task>> tasksByInstance = new IdentityHashMap<>(); @Inject MatchScheduler(Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler, Match match) { this(MatchScope.LOADED, loggers, backend, exceptionHandler, match); } MatchScheduler(MatchScope scope, Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler, Match match) { super(loggers, backend, exceptionHandler, false); this.match = match; this.scope = scope; } void registerRepeatables(final Object object) { tasksByInstance.computeIfAbsent(object, o -> repeatableMethodsByClass .getUnchecked(object.getClass()) .stream() .filter(rm -> rm.scope == scope) .map(repeatable -> { MethodHandle handle = repeatable.handle.bindTo(object); if(handle.type().parameterCount() > 0) { handle = handle.bindTo(match); } final MethodHandle finalHandle = handle; return register(repeatable.parameters, (ThrowingRunnable<?>) finalHandle::invokeExact, null); }) .collect(Collectors.toImmutableSet()) ); } void unregisterRepeatables(final Object object) { final Set<Task> set = tasksByInstance.remove(object); if(set != null) { set.forEach(Task::cancel); } } private static class RepeatableMethod { final MethodHandle handle; final Task.Parameters parameters; final MatchScope scope; RepeatableMethod(MethodHandle handle, Task.Parameters parameters, MatchScope scope) { this.handle = handle; this.parameters = parameters; this.scope = scope; } } private static final LoadingCache<Class<?>, ImmutableSet<RepeatableMethod>> repeatableMethodsByClass = CacheUtils.newCache(cls -> { final ImmutableSet.Builder<RepeatableMethod> methods = ImmutableSet.builder(); Methods.declaredMethodsInAncestors(cls).forEach(method -> { final Repeatable annotation = method.getAnnotation(Repeatable.class); if(annotation != null) { method.setAccessible(true); final Class<?>[] parameters = method.getParameterTypes(); if(!(parameters.length == 0 || (parameters.length == 1 && Match.class.isAssignableFrom(parameters[0])))) { throw new IllegalArgumentException(method.getName() + " does not have compatible parameter types (" + Joiner.on(",").join(parameters) + ")"); } try { methods.add(new RepeatableMethod( MethodHandles.publicLookup().unreflect(method), Task.Parameters.fromDuration(Duration.ZERO, Time.convertTo.duration(annotation.interval())), annotation.scope() )); } catch(IllegalAccessException e) { throw new IllegalStateException("Failed to get handle for repeatable method " + method, e); } } }); return methods.build(); }); }