Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event system move to MegaMek #135

Merged
merged 3 commits into from
Apr 17, 2016
Merged
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
170 changes: 170 additions & 0 deletions megamek/src/megamek/common/event/EventBus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* EventBus.java - Simple event bus implementation
*
* Copyright (C) 2016 MegaMek Team
*
* This file is part of MegaMek
*
* Some rights reserved. See megamek/docs/license.txt
*/

package megamek.common.event;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class EventBus {
private static final Object INSTANCE_LOCK = new Object[0];

private static EventBus instance;
private static final EventSorter EVENT_SORTER = new EventSorter();

private final Object REGISTER_LOCK = new Object[0];

private ConcurrentHashMap<Object, List<EventListener>> handlerMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<Class<? extends MMEvent>, List<EventListener>> eventMap = new ConcurrentHashMap<>();
// There is no Java-supplied IdentityHashSet ...
private Map<Object, Object> unregisterQueue = new IdentityHashMap<>();

public static EventBus getInstance() {
synchronized(INSTANCE_LOCK) {
if(null == instance) {
instance = new EventBus();
}
}
return instance;
}

public static void registerHandler(Object handler) {
getInstance().register(handler);
}

public static void unregisterHandler(Object handler) {
getInstance().unregister(handler);
}

public static boolean triggerEvent(MMEvent event) {
return getInstance().trigger(event);
}

public EventBus() {}

private List<Class<?>> getClasses(Class<?> leaf) {
List<Class<?>> result = new ArrayList<>();
while(null != leaf) {
result.add(leaf);
leaf = leaf.getSuperclass();
}
return result;
}

@SuppressWarnings("unchecked")
public void register(Object handler) {
if(handlerMap.containsKey(handler)) {
return;
}

for(Method method : handler.getClass().getMethods()) {
for(Class<?> cls : getClasses(handler.getClass())) {
try {
Method realMethod = cls.getDeclaredMethod(method.getName(), method.getParameterTypes());
if(realMethod.isAnnotationPresent(Subscribe.class)) {
Class<?>[] parameterTypes = method.getParameterTypes();
if(parameterTypes.length != 1) {
throw new IllegalArgumentException(
String.format("@Subscribe annotation requires single-argument method; %s has %d", //$NON-NLS-1$
method, parameterTypes.length));
}
Class<?> eventType = parameterTypes[0];
if(!MMEvent.class.isAssignableFrom(eventType)) {
throw new IllegalArgumentException(
String.format("@Subscribe annotation of %s requires the argument type to be some subtype of MMEvent, not %s", //$NON-NLS-1$
method, eventType));
}
internalRegister(handler, realMethod, (Class<? extends MMEvent>) eventType);
}
} catch (NoSuchMethodException e) {
// ignore
}
}
}
}

private void internalRegister(Object handler, Method method, Class<? extends MMEvent> eventType) {
synchronized(REGISTER_LOCK) {
EventListener listener = new EventListener(handler, method, eventType);
List<EventListener> handlerListeners = handlerMap.get(handler);
if(null == handlerListeners) {
handlerListeners = new ArrayList<>();
handlerMap.put(handler, handlerListeners);
}
handlerListeners.add(listener);
List<EventListener> eventListeners = eventMap.get(eventType);
if(null == eventListeners) {
eventListeners = new ArrayList<>();
eventMap.put(eventType, eventListeners);
}
eventListeners.add(listener);
}
}

public void unregister(Object handler) {
synchronized(REGISTER_LOCK) {
unregisterQueue.put(handler, handler);
}
}

private void internalUnregister() {
synchronized(REGISTER_LOCK) {
for(Object handler : unregisterQueue.keySet()) {
List<EventListener> listenerList = handlerMap.remove(handler);
if(null != listenerList) {
for(EventListener listener : listenerList) {
List<EventListener> eventListeners = eventMap.get(listener.getEventType());
if(null != eventListeners) {
eventListeners.remove(listener);
}
}
}
}
unregisterQueue.clear();
}
}

/** @return true if the event was cancelled along the way */
@SuppressWarnings("unchecked")
public boolean trigger(MMEvent event) {
internalUnregister(); // Clean up unregister queue
for(Class<?> cls : getClasses(event.getClass())) {
if(MMEvent.class.isAssignableFrom(cls)) {
// Run through the triggers for each superclass up to MMEvent itself
internalTrigger((Class<? extends MMEvent>) cls, event);
}
}
return event.isCancellable() ? event.isCancelled() : false;
}

private void internalTrigger(Class<? extends MMEvent> eventClass, MMEvent event) {
List<EventListener> eventListeners = eventMap.get(eventClass);
if(null != eventListeners) {
Collections.sort(eventListeners, EVENT_SORTER);
for(EventListener listener : eventListeners) {
listener.trigger(event);
}
}
}

private static class EventSorter implements Comparator<EventListener> {
@Override
public int compare(EventListener el1, EventListener el2) {
// Highest to lowest, by priority
return Integer.compare(el2.getPriority(), el1.getPriority());
}
}
}
51 changes: 51 additions & 0 deletions megamek/src/megamek/common/event/EventListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* EventListener.java - Simple event system helper class
*
* Copyright (C) 2016 MegaMek Team
*
* This file is part of MegaMek
*
* Some rights reserved. See megamek/docs/license.txt
*/

package megamek.common.event;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

class EventListener {
private final Object handler;
private final Method method;
private final Class<? extends MMEvent> eventType;
private final Subscribe info;

public EventListener(Object handler, Method method, Class<? extends MMEvent> eventType) {
this.handler = Objects.requireNonNull(handler);
this.method = Objects.requireNonNull(method);
this.eventType = Objects.requireNonNull(eventType);
this.info = method.getAnnotation(Subscribe.class);
}

public void trigger(MMEvent event) {
if(!event.isCancellable() || !event.isCancelled()) {
try {
method.invoke(handler, event);
} catch(IllegalAccessException e) {
e.printStackTrace();
} catch(IllegalArgumentException e) {
e.printStackTrace();
} catch(InvocationTargetException e) {
e.printStackTrace();
}
}
}

public int getPriority() {
return info.priority();
}

public Class<? extends MMEvent> getEventType() {
return eventType;
}
}
37 changes: 37 additions & 0 deletions megamek/src/megamek/common/event/MMEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* MMEvent.java - Simple event system helper class
*
* Copyright (C) 2016 MegaMek Team
*
* This file is part of MegaMek
*
* Some rights reserved. See megamek/docs/license.txt
*/

package megamek.common.event;

/**
* Base class for all events
*/
public abstract class MMEvent {
protected boolean cancelled = false;

public MMEvent() {
}

/** @return true if the event can be cancelled (aborted) */
public boolean isCancellable() {
return false;
}

/** @return true if the event is cancelled */
public boolean isCancelled() {
return cancelled;
}

public void cancel() {
if(isCancellable()) {
cancelled = true;
}
}
}
40 changes: 40 additions & 0 deletions megamek/src/megamek/common/event/Subscribe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* MMEvent.java - Simple event system helper annotation
*
* Copyright (C) 2016 MegaMek Team
*
* This file is part of MegaMek
*
* Some rights reserved. See megamek/docs/license.txt
*/

package megamek.common.event;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to put on public methods which wish to receive events.
* <p>
* A method annotated with this needs to have exactly one argument,
* this being some subclass of {@link MMEvent}.
* An instance of that class then needs to be registered with the event bus
* via {@link EventBus#registerHandler(Object)} for it to work. The exact
* name of the method is not important, and neither is how many of
* such methods are packed into a single class.
* <p>
* It's a good idea (but not required) to keep a reference to
* the instance containing the event handlers yourself after registering it,
* if only to avoid registering it multiple times.
* <p>
* To avoid resource leaks, event handlers need be explicitly unregistered.
* They can do this safely in their event handler methods.
*/
@Retention(value=RetentionPolicy.RUNTIME)
@Target(value=ElementType.METHOD)
public @interface Subscribe {
/** Priority of the event handler, default 0 */
public int priority() default 0;
}