/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NOVA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOVA. If not, see <http://www.gnu.org/licenses/>.
*/
package nova.core.event.bus;
import nova.internal.core.util.TopologicalSort;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* A general purpose event bus. This class is thread-safe and listeners can be
* added or removed concurrently, no external locking is ever needed.
* @param <T> event type
* @author Stan Hebben, Calclavia
*/
public class EventBus<T> {
public static final int PRIORITY_HIGH = 100;
public static final int PRIORITY_DEFAULT = 0;
public static final int PRIORITY_LOW = -100;
// TODO: actually test concurrency
protected final List<EventListenerNode> unsortedListeners = new ArrayList<>();
private List<EventListenerNode> sortedListeners;
/**
* Builds an ordered list cachedListeners. Sorts using topological sort algorithm.
*/
protected synchronized void buildCache() {
TopologicalSort.DirectedGraph<EventListenerNode> graph = new TopologicalSort.DirectedGraph<>();
unsortedListeners.forEach(graph::addNode);
//Create directed graph edges.
unsortedListeners.forEach(
node -> {
//Sort "after"
node.after.forEach(
name ->
unsortedListeners
.stream()
.filter(node2 -> node2.name.equals(name))
.findFirst()
.ifPresent(dependent -> graph.addEdge(dependent, node))
);
//Sort "before"
node.before.forEach(
name ->
unsortedListeners
.stream()
.filter(node2 -> node2.name.equals(name))
.findFirst()
.ifPresent(dependent -> graph.addEdge(node, dependent))
);
//Priority check
unsortedListeners
.stream()
.filter(compare -> node.priority < compare.priority)
.forEach(compare -> graph.addEdge(compare, node));
}
);
sortedListeners = TopologicalSort.topologicalSort(graph);
}
/**
* Invalidates the sorted listeners. Call this after tweaking with unsortedListeners.
*/
protected synchronized void invalidateCache() {
sortedListeners = null;
}
/**
* Retrieves the sorted listeners. Calls buildCache() if the listeners aren't sorted.
* @return The sorted listeners.
*/
protected synchronized List<EventListenerNode> getSortedListeners() {
if (sortedListeners == null) {
buildCache();
}
return sortedListeners;
}
public synchronized void clear() {
unsortedListeners.clear();
invalidateCache();
}
/**
* Removes an EventListener from the list.
* @param listener listener to be removed
* @return true if the listener was removed, false it it wasn't there
*/
public synchronized boolean remove(EventListener<T> listener) {
boolean didRemove = unsortedListeners.removeIf(node -> node.getListener().equals(listener));
if (didRemove) {
invalidateCache();
}
return didRemove;
}
/**
* Checks if there are any listeners in this list.
* @return true if empty
*/
public boolean isEmpty() {
return count() == 0;
}
public int count() {
return unsortedListeners.size();
}
/**
* Publishes an event by calling all of the registered listeners.
* @param event event to be published
*/
public void publish(T event) {
getSortedListeners()
.stream()
.forEachOrdered(node -> node.getListener().onEvent(event));
}
/**
* Retrieves the EventBinder object to bind an EventListener to ths EventBus that only accepts a specific subclass of <T>
* @param <E> The event type
* @return event listener's handle
*/
public <E extends T> EventBinder<E> on() {
return new EventBinder<>(Optional.empty());
}
public <E extends T> EventBinder<E> on(Class<E> clazz) {
return new EventBinder<>(Optional.of(clazz));
}
public class EventBinder<E extends T> {
private final Optional<Class<E>> clazz;
private int priority = PRIORITY_DEFAULT;
private String name;
private Set<String> before = new HashSet<>();
private Set<String> after = new HashSet<>();
public EventBinder(Optional<Class<E>> clazz) {
this.clazz = clazz;
}
/**
* Sets the event's numeric priority.
* Numeric priority overrules named priority.
* @param priority An integer. The higher the number, the higher the priority.
* @return This
*/
public EventBinder<E> withPriority(int priority) {
this.priority = priority;
return this;
}
/**
* Sets the event to have a name.
* @param name The event name.
* @return This
*/
public EventBinder<E> withName(String name) {
this.name = name;
return this;
}
/**
* Sets the event to occur before another event with given name.
* @param name The other event name
* @return This
*/
public EventBinder<E> before(String name) {
before.add(name);
return this;
}
/**
* Sets the event to occur after another event with given name.
* @param name The other event name
* @return This
*/
public EventBinder<E> after(String name) {
after.add(name);
return this;
}
/**
* Binds the event to the {@link EventBus}, finalizing all modifiers on the event.
* @param list Event listener
* @return The event handler
*/
public synchronized EventListenerHandle<T> bind(EventListener<E> list) {
EventListener<T> listener = clazz.isPresent() ? new TypedEventListener<>(list, clazz.get()) : (EventListener) list;
if (name != null && unsortedListeners.stream().filter(node -> node.name != null).anyMatch(node -> node.name.equals(name))) {
throw new EventException("Duplicate event listener name: " + name);
}
EventListenerNode node = new EventListenerNode(listener, name, priority, before, after);
unsortedListeners.add(node);
invalidateCache();
return node;
}
}
// #########################
// ### Protected classes ###
// #########################
/**
* A wrapper for an event listener that only accepts a specific type of
* event.
* @param <E> event type
* @param <T> super type
* @author Vic Nightfall
*/
protected static class TypedEventListener<E extends T, T> implements EventListener<T> {
private final Class<E> eventClass;
private final EventListener<E> wrappedListener;
/**
* Constructs a new single typed Event listener.
* @param wrappedListener The listener which gets called when the event
* was accepted.
* @param eventClass The event to listen for, Any posted event that is
* an instance of said class will get passed through to the
* wrapped listener instance.
*/
public TypedEventListener(EventListener<E> wrappedListener, Class<E> eventClass) {
this.eventClass = eventClass;
this.wrappedListener = wrappedListener;
}
@SuppressWarnings("unchecked")
@Override
public void onEvent(T event) {
if (eventClass.isInstance(event)) {
wrappedListener.onEvent((E) event);
}
}
}
protected class EventListenerNode implements EventListenerHandle<T> {
protected final EventListener<T> listener;
protected final int priority;
protected final String name;
protected final Set<String> before;
protected final Set<String> after;
public EventListenerNode(EventListener<T> handler, String name, int priority, Set<String> before, Set<String> after) {
this.listener = handler;
this.name = name;
this.priority = priority;
this.before = before;
this.after = after;
}
@Override
public EventListener<T> getListener() {
return listener;
}
@Override
public void close() {
synchronized (EventBus.this) {
unsortedListeners.remove(this);
invalidateCache();
}
}
}
}