From 76a964a9d37c95d237a462a0bd92b03e78515245 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 31 Oct 2018 08:27:32 +0100 Subject: [PATCH] Add EventBus to replace Guava one Signed-off-by: Tomas Slusny --- runelite-api/pom.xml | 5 +- .../net/runelite/api/events/EventBus.java | 249 ++++++++++++++++++ .../net/runelite/api/events/Subscribe.java | 41 +++ 3 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 runelite-api/src/main/java/net/runelite/api/events/EventBus.java create mode 100644 runelite-api/src/main/java/net/runelite/api/events/Subscribe.java diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml index 8a9e045c79..8ce33f383f 100644 --- a/runelite-api/pom.xml +++ b/runelite-api/pom.xml @@ -46,9 +46,8 @@ provided - com.google.code.findbugs - jsr305 - 1.3.9 + com.google.guava + guava diff --git a/runelite-api/src/main/java/net/runelite/api/events/EventBus.java b/runelite-api/src/main/java/net/runelite/api/events/EventBus.java new file mode 100644 index 0000000000..7277a3a0af --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/EventBus.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2018, Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.api.events; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.ThreadSafe; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +@ThreadSafe +public class EventBus +{ + @FunctionalInterface + public interface SubscriberMethod + { + void invoke(Object event); + } + + @Value + private static class Subscriber + { + private final Object object; + private final Method method; + @EqualsAndHashCode.Exclude + private final SubscriberMethod lamda; + + void invoke(final Object arg) throws Exception + { + if (lamda != null) + { + lamda.invoke(arg); + } + else + { + method.invoke(object, arg); + } + } + } + + private final Consumer exceptionHandler; + private ImmutableMultimap subscribers = ImmutableMultimap.of(); + + /** + * Instantiates EventBus with default exception handler + */ + public EventBus() + { + this((e) -> log.warn("Uncaught exception in event subscriber", e)); + } + + /** + * Registers subscriber to EventBus. All methods in subscriber and it's parent classes are checked for + * {@link Subscribe} annotation and then added to map of subscriptions. + * + * @param object subscriber to register + * @throws IllegalArgumentException in case subscriber method name is wrong (correct format is 'on' + EventName + */ + public synchronized void register(@Nonnull final Object object) + { + final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + + if (subscribers != null) + { + builder.putAll(subscribers); + } + + for (Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) + { + for (final Method method : clazz.getDeclaredMethods()) + { + final Subscribe sub = method.getAnnotation(Subscribe.class); + + if (sub == null) + { + continue; + } + + Preconditions.checkArgument(method.getReturnType() == Void.TYPE, "@Subscribed method \"" + method + "\" cannot return a value"); + Preconditions.checkArgument(method.getParameterCount() == 1, "@Subscribed method \"" + method + "\" must take exactly 1 argument"); + Preconditions.checkArgument(!Modifier.isStatic(method.getModifiers()), "@Subscribed method \"" + method + "\" cannot be static"); + + final Class parameterClazz = method.getParameterTypes()[0]; + + Preconditions.checkArgument(!parameterClazz.isPrimitive(), "@Subscribed method \"" + method + "\" cannot subscribe to primitives"); + Preconditions.checkArgument((parameterClazz.getModifiers() & (Modifier.ABSTRACT | Modifier.INTERFACE)) == 0, "@Subscribed method \"" + method + "\" cannot subscribe to polymorphic classes"); + + for (Class psc = parameterClazz.getSuperclass(); psc != null; psc = psc.getSuperclass()) + { + if (subscribers.containsKey(psc)) + { + throw new IllegalArgumentException("@Subscribed method \"" + method + "\" cannot subscribe to class which inherits from subscribed class \"" + psc + "\""); + } + } + + final String preferredName = "on" + parameterClazz.getSimpleName(); + Preconditions.checkArgument(method.getName().equals(preferredName), "Subscribed method " + method + " should be named " + preferredName); + + method.setAccessible(true); + SubscriberMethod lambda = null; + + try + { + final MethodHandles.Lookup caller = privateLookupIn(clazz); + final MethodType subscription = MethodType.methodType(void.class, parameterClazz); + final MethodHandle target = caller.findVirtual(clazz, method.getName(), subscription); + final CallSite site = LambdaMetafactory.metafactory( + caller, + "invoke", + MethodType.methodType(SubscriberMethod.class, clazz), + subscription.changeParameterType(0, Object.class), + target, + subscription); + + final MethodHandle factory = site.getTarget(); + lambda = (SubscriberMethod) factory.bindTo(object).invokeExact(); + } + catch (Throwable e) + { + log.warn("Unable to create lambda for method {}", method, e); + } + + final Subscriber subscriber = new Subscriber(object, method, lambda); + builder.put(parameterClazz, subscriber); + log.debug("Registering {} - {}", parameterClazz, subscriber); + } + } + + subscribers = builder.build(); + } + + /** + * Unregisters all subscribed methods from provided subscriber object. + * + * @param object object to unsubscribe from + */ + public synchronized void unregister(@Nonnull final Object object) + { + if (subscribers == null) + { + return; + } + + final Multimap map = HashMultimap.create(); + map.putAll(subscribers); + + for (Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) + { + for (final Method method : clazz.getDeclaredMethods()) + { + final Subscribe sub = method.getAnnotation(Subscribe.class); + + if (sub == null) + { + continue; + } + + final Class parameterClazz = method.getParameterTypes()[0]; + map.remove(parameterClazz, new Subscriber(object, method, null)); + } + } + + subscribers = ImmutableMultimap.copyOf(map); + } + + /** + * Posts provided event to all registered subscribers. Subscriber calls are invoked immediately and in order + * in which subscribers were registered. + * + * @param event event to post + */ + public void post(@Nonnull final Object event) + { + for (final Subscriber subscriber : subscribers.get(event.getClass())) + { + try + { + subscriber.invoke(event); + } + catch (Exception e) + { + exceptionHandler.accept(e); + } + } + } + + private static MethodHandles.Lookup privateLookupIn(Class clazz) throws IllegalAccessException, NoSuchFieldException, InvocationTargetException + { + try + { + // Java 9+ has privateLookupIn method on MethodHandles, but since we are shipping and using Java 8 + // we need to access it via reflection. This is preferred way because it's Java 9+ public api and is + // likely to not change + final Method privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class); + return (MethodHandles.Lookup) privateLookupIn.invoke(null, clazz, MethodHandles.lookup()); + } + catch (NoSuchMethodException e) + { + // In Java 8 we first do standard lookupIn class + final MethodHandles.Lookup lookupIn = MethodHandles.lookup().in(clazz); + + // and then we mark it as trusted for private lookup via reflection on private field + final Field modes = MethodHandles.Lookup.class.getDeclaredField("allowedModes"); + modes.setAccessible(true); + modes.setInt(lookupIn, -1); // -1 == TRUSTED + return lookupIn; + } + } +} diff --git a/runelite-api/src/main/java/net/runelite/api/events/Subscribe.java b/runelite-api/src/main/java/net/runelite/api/events/Subscribe.java new file mode 100644 index 0000000000..4c70602f83 --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/Subscribe.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018, Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.api.events; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as an event subscriber. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Subscribe +{ +}