Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group "meteordevelopment"
version "0.2.5"
version "0.3.0"

sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_8

Expand Down
189 changes: 107 additions & 82 deletions src/main/java/meteordevelopment/orbit/EventBus.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,40 @@
import meteordevelopment.orbit.listeners.IListener;
import meteordevelopment.orbit.listeners.LambdaListener;

import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
import java.util.function.Consumer;

/**
* Default implementation of {@link IEventBus}.
*/
public class EventBus implements IEventBus {
private static class LambdaFactoryInfo {
public final String packagePrefix;
public final LambdaListener.Factory factory;
private static final Map<Method, WeakReference<MethodHandle>> LAMBDA_FACTORY_CACHE = Collections.synchronizedMap(new WeakHashMap<>());

public LambdaFactoryInfo(String packagePrefix, LambdaListener.Factory factory) {
this.packagePrefix = packagePrefix;
this.factory = factory;
}
}

private final Map<Object, List<IListener>> listenerCache = new ConcurrentHashMap<>();
private final Map<Object, List<IListener>> listenerCache = Collections.synchronizedMap(new IdentityHashMap<>());
private final Map<Class<?>, List<IListener>> staticListenerCache = new ConcurrentHashMap<>();

private final Map<Class<?>, List<IListener>> listenerMap = new ConcurrentHashMap<>();

private final List<LambdaFactoryInfo> lambdaFactoryInfos = new ArrayList<>();
private final List<LookupInfo> lookupInfos = new ArrayList<>();

@Override
public void registerLambdaFactory(String packagePrefix, LambdaListener.Factory factory) {
synchronized (lambdaFactoryInfos) {
lambdaFactoryInfos.add(new LambdaFactoryInfo(packagePrefix, factory));
public void registerLookup(String packagePrefix, MethodHandles.Lookup lookup) {
synchronized (lookupInfos) {
// to ensure the lookups are used correctly, they are ordered from longest to shortest
int i = 0;
while (i < lookupInfos.size() && lookupInfos.get(i).packagePrefix.length() > packagePrefix.length()) {
i++;
}
lookupInfos.add(i, new LookupInfo(packagePrefix, lookup));
}
}

Expand All @@ -45,6 +46,22 @@ public boolean isListening(Class<?> eventKlass) {
return listeners != null && !listeners.isEmpty();
}

@Override
public boolean isSubscribed(Object object) {
return listenerCache.containsKey(object);
}

@Override
public boolean isSubscribed(Class<?> klass) {
return staticListenerCache.containsKey(klass);
}

@Override
public boolean isSubscribed(IListener listener) {
List<IListener> listeners = listenerMap.get(listener.getTarget());
return listeners != null && listeners.contains(listener);
}

@Override
public <T> T post(T event) {
List<IListener> listeners = listenerMap.get(event.getClass());
Expand Down Expand Up @@ -74,30 +91,21 @@ public <T extends ICancellable> T post(T event) {

@Override
public void subscribe(Object object) {
subscribe(getListeners(object.getClass(), object), false);
subscribe(listenerCache.computeIfAbsent(object, o -> createListeners(o.getClass(), o)));
}

@Override
public void subscribe(Class<?> klass) {
subscribe(getListeners(klass, null), true);
}

@Override
public void subscribe(IListener listener) {
subscribe(listener, false);
subscribe(staticListenerCache.computeIfAbsent(klass, k -> createListeners(k, null)));
}

private void subscribe(List<IListener> listeners, boolean onlyStatic) {
for (IListener listener : listeners) subscribe(listener, onlyStatic);
private void subscribe(List<IListener> listeners) {
for (IListener listener : listeners) subscribe(listener);
}

private void subscribe(IListener listener, boolean onlyStatic) {
if (onlyStatic) {
if (listener.isStatic()) insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener);
}
else {
insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener);
}
@Override
public void subscribe(IListener listener) {
insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener);
}

private void insert(List<IListener> listeners, IListener listener) {
Expand All @@ -111,63 +119,80 @@ private void insert(List<IListener> listeners, IListener listener) {

@Override
public void unsubscribe(Object object) {
unsubscribe(getListeners(object.getClass(), object), false);
List<IListener> listeners = listenerCache.remove(object);
if (listeners != null) unsubscribe(listeners);
// for backwards-compatibility
else unsubscribe(object.getClass());
}

@Override
public void unsubscribe(Class<?> klass) {
unsubscribe(getListeners(klass, null), true);
}

@Override
public void unsubscribe(IListener listener) {
unsubscribe(listener, false);
List<IListener> staticListeners = staticListenerCache.remove(klass);
if (staticListeners != null) unsubscribe(staticListeners);
}

private void unsubscribe(List<IListener> listeners, boolean staticOnly) {
for (IListener listener : listeners) unsubscribe(listener, staticOnly);
private void unsubscribe(List<IListener> listeners) {
for (IListener listener : listeners) unsubscribe(listener);
}

private void unsubscribe(IListener listener, boolean staticOnly) {
@Override
public void unsubscribe(IListener listener) {
List<IListener> l = listenerMap.get(listener.getTarget());

if (l != null) {
if (staticOnly) {
if (listener.isStatic()) l.remove(listener);
if (l != null) l.remove(listener);
}

private List<IListener> createListeners(Class<?> klass, Object object) {
List<IListener> listeners = new ArrayList<>();

while (klass != Object.class) {
MethodHandles.Lookup lookup = null;

for (Method method : klass.getDeclaredMethods()) {
// skip invalid methods
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (!isValid(method) || (object == null && !isStatic)) {
continue;
}

// get or create lambda factory
WeakReference<MethodHandle> ref = LAMBDA_FACTORY_CACHE.get(method);
MethodHandle lambdaFactory = ref != null ? ref.get() : null;
try {
if (lambdaFactory == null) {
// lazily search for lookup infos
if (lookup == null) {
lookup = getLookupInfo(klass).in(klass);
}

lambdaFactory = LambdaMetafactory.metafactory(
lookup, "accept",
isStatic ? MethodType.methodType(Consumer.class) : MethodType.methodType(Consumer.class, klass),
MethodType.methodType(void.class, Object.class),
lookup.unreflect(method),
MethodType.methodType(void.class, method.getParameters()[0].getType())
).getTarget();

if (!isStatic) {
lambdaFactory = lambdaFactory.asType(MethodType.methodType(Consumer.class, Object.class));
}

LAMBDA_FACTORY_CACHE.put(method, new WeakReference<>(lambdaFactory));
}

listeners.add(new LambdaListener(lambdaFactory, object, method));
} catch (Throwable throwable) {
String message = String.format(
"Could not create lambda listener for '%s.%s(%s)'.",
klass.getSimpleName(), method.getName(), method.getParameters()[0].getType().getSimpleName()
);
throw new IllegalStateException(message, throwable);
}
}
else l.remove(listener);
}
}

private List<IListener> getListeners(Class<?> klass, Object object) {
Function<Object, List<IListener>> func = o -> {
List<IListener> listeners = new CopyOnWriteArrayList<>();

getListeners(listeners, klass, object);

return listeners;
};

if (object == null) return staticListenerCache.computeIfAbsent(klass, func);

// We need to check if the instances are the same and avoid using .equals() and .hashCode()
for (Object key : listenerCache.keySet()) {
if (key == object) return listenerCache.get(object);
}

List<IListener> listeners = func.apply(object);
listenerCache.put(object, listeners);
return listeners;
}

private void getListeners(List<IListener> listeners, Class<?> klass, Object object) {
for (Method method : klass.getDeclaredMethods()) {
if (isValid(method)) {
listeners.add(new LambdaListener(getLambdaFactory(klass), klass, object, method));
}
klass = klass.getSuperclass();
}

if (klass.getSuperclass() != null) getListeners(listeners, klass.getSuperclass(), object);
return new CopyOnWriteArrayList<>(listeners);
}

private boolean isValid(Method method) {
Expand All @@ -178,10 +203,10 @@ private boolean isValid(Method method) {
return !method.getParameters()[0].getType().isPrimitive();
}

private LambdaListener.Factory getLambdaFactory(Class<?> klass) {
synchronized (lambdaFactoryInfos) {
for (LambdaFactoryInfo info : lambdaFactoryInfos) {
if (klass.getName().startsWith(info.packagePrefix)) return info.factory;
private MethodHandles.Lookup getLookupInfo(Class<?> klass) {
synchronized (lookupInfos) {
for (LookupInfo info : lookupInfos) {
if (klass.getName().startsWith(info.packagePrefix)) return info.lookup;
}
}

Expand Down
34 changes: 30 additions & 4 deletions src/main/java/meteordevelopment/orbit/IEventBus.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package meteordevelopment.orbit;

import meteordevelopment.orbit.listeners.IListener;
import meteordevelopment.orbit.listeners.LambdaListener;

import java.lang.invoke.MethodHandles;

/**
* Manages event listeners.
*/
public interface IEventBus {
/**
* Registers a lambda factory to use with the specified package.
* Registers a lookup allowing orbit to reflect into private members inside the provided package. You can obtain a
* lookup instance by calling {@link MethodHandles#lookup()}.
* @param packagePrefix Package prefix that this factory will be used for, eg "meteordevelopment.orbit"
* @param factory The factory to use
* @param lookup The lookup to use.
*/
void registerLambdaFactory(String packagePrefix, LambdaListener.Factory factory);
void registerLookup(String packagePrefix, MethodHandles.Lookup lookup);

/**
* Returns whether at least one event listener is currently registered for this event type.
Expand All @@ -22,6 +24,30 @@ public interface IEventBus {
*/
boolean isListening(Class<?> eventClass);

/**
* Returns whether the object is currently subscribed to the event bus.
* @param object The object to query
* @return whether the object is currently subscribed to the event bus
* @since 0.3.0
*/
boolean isSubscribed(Object object);

/**
* Returns whether the class is currently subscribed to the event bus.
* @param klass The class to query
* @return whether the class is currently subscribed to the event bus
* @since 0.3.0
*/
boolean isSubscribed(Class<?> klass);

/**
* Returns whether the listener is currently subscribed to the event bus.
* @param listener The listener to query
* @return whether the listener is currently subscribed to the event bus
* @since 0.3.0
*/
boolean isSubscribed(IListener listener);

/**
* Posts an event to all subscribed event listeners.
* @param event Event to post
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/meteordevelopment/orbit/LookupInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package meteordevelopment.orbit;

import java.lang.invoke.MethodHandles;

public class LookupInfo {
public final String packagePrefix;
public final MethodHandles.Lookup lookup;

public LookupInfo(String packagePrefix, MethodHandles.Lookup lookup) {
this.packagePrefix = packagePrefix;
this.lookup = lookup;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,4 @@ public Class<T> getTarget() {
public int getPriority() {
return priority;
}

@Override
public boolean isStatic() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,4 @@ public interface IListener {
* @return The priority for this listener
*/
int getPriority();

/**
* @return True if this listener is for static methods
* @deprecated Will be removed in a future version
*/
@Deprecated
boolean isStatic();
}
Loading