package org.threadly.concurrent.event;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.Executor;
import org.threadly.util.ExceptionUtils;
/**
* Simple utility for multiplying invocations across multiple instances of a given interface.
*
* @since 4.3.0
*/
@SuppressWarnings("unchecked")
public class InvocationTee {
/**
* This creates a tee proxy for a given class with a set of instances of said interface. Any
* invocation to the returned instance (proxy), will be multiple to all provided instances. If
* provided arguments are mutable, and an instance mutates that argument, future invoked
* instances may see that modification. It is not deterministic which order the instances will
* be invoked in.
* <p>
* If any listeners throw an exception it will be delegated to
* {@link org.threadly.util.ExceptionUtils#handleException(Throwable)}. So a listener throwing
* an exception will NOT interrupt other listeners from being invoked. If you want you can
* handle thrown exceptions by setting an {@link org.threadly.util.ExceptionHandler} into
* {@link org.threadly.util.ExceptionUtils}. Make sure the handler is set it in a way that makes
* sense based off what thread will be invoking the returned interface.
* <p>
* Under the hood this depends on {@link ListenerHelper}, and thus has the same limitations.
* Most specifically the provided {@code teeInterface} must in fact be an interface, and not a
* an abstract class, or other non-interface types. In addition any invocations called to this
* must be a {@code void} return type.
*
* @param <T> Type representing interface to multiple invocations of
* @param teeInterface Interface class for which the returned instance must implement
* @param instances Instances of said interface which invocations should be multiplied to
* @return A returned interface which will map all invocations to all provided interfaces
*/
public static <T> T tee(Class<? super T> teeInterface, T ... instances) {
return setupHelper(new ListenerHelper<>(teeInterface), instances);
}
/**
* This implementation is variation from {@link #tee(Class, Object...)}, read that documentation
* first.
* <p>
* The behavior modifies from that implementation is in how exceptions are handled. Rather than
* handling them and ensuring that all instances are invoked, this will throw the exception.
* More specifically when an instance throws an exception it will prevent further instances from
* bring invoked, and that exception will bubble up to the original interface invoker.
*
* @param <T> Type representing interface to multiple invocations of
* @param teeInterface Interface class for which the returned instance must implement
* @param instances Instances of said interface which invocations should be multiplied to
* @return A returned interface which will map all invocations to all provided interfaces
*/
public static <T> T teeWithExceptionThrowing(Class<? super T> teeInterface, T ... instances) {
ListenerHelper<T> lh = new ListenerHelper<T>(teeInterface) {
@Override
protected T makeProxyInstance(Class<? super T> listenerInterface) {
return (T) Proxy.newProxyInstance(listenerInterface.getClassLoader(),
new Class<?>[] { listenerInterface },
new ListenerCaller() {
@Override
protected void callListener(T listener, Method method, Object[] args) {
try {
method.invoke(listener, args);
} catch (IllegalAccessException e) {
/* should not be possible since only interfaces are allowed, and
* all functions in interfaces are public
*/
ExceptionUtils.handleException(e);
} catch (InvocationTargetException e) {
// throw exception to interrupt calling handlers
throw ExceptionUtils.makeRuntime(e.getCause());
}
}
});
}
};
return setupHelper(lh, instances);
}
/**
* This implementation is variation from {@link #tee(Class, Object...)}, read that documentation
* first.
* <p>
* The behavior modifies from that implementation is that all instances will be called by a
* executor. This means that invocations into the returned instance will return immediately,
* but instance invocation will happen async. It is guaranteed that any single instance will NOT
* be invoked in parallel. Thus meaning that if you invoke into the proxy two times before
* a give instance finishes processing the first invocation, the second invocation will queue and
* only be invoked after the first finishes.
* <p>
* Under the hood {@link DefaultExecutorListenerHelper} is used to provide this behavior.
*
* @param <T> Type representing interface to multiple invocations of
* @param executor Executor that instances will be invoked on to
* @param teeInterface Interface class for which the returned instance must implement
* @param instances Instances of said interface which invocations should be multiplied to
* @return A returned interface which will map all invocations to all provided interfaces
*/
public static <T> T teeWithExecutor(Executor executor,
Class<? super T> teeInterface, T ... instances) {
return setupHelper(new DefaultExecutorListenerHelper<>(teeInterface, executor), instances);
}
private static <T> T setupHelper(ListenerHelper<T> lh, T ... instances) {
for (T instance : instances) {
lh.addListener(instance);
}
return lh.call();
}
}