Initial source import.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package mightypork.utils.eventbus;
|
||||
|
||||
|
||||
/**
|
||||
* Access to an {@link EventBus} instance
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public interface BusAccess {
|
||||
|
||||
/**
|
||||
* @return event bus
|
||||
*/
|
||||
EventBus getEventBus();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package mightypork.utils.eventbus;
|
||||
|
||||
|
||||
import mightypork.utils.eventbus.events.flags.DelayedEvent;
|
||||
import mightypork.utils.eventbus.events.flags.DirectEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NonConsumableEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NotLoggedEvent;
|
||||
import mightypork.utils.eventbus.events.flags.SingleReceiverEvent;
|
||||
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Event that can be handled by HANDLER, subscribing to the event bus.
|
||||
* </p>
|
||||
* <p>
|
||||
* Can be annotated as {@link SingleReceiverEvent} to be delivered once only,
|
||||
* and {@link DelayedEvent} or {@link DirectEvent} to specify default sending
|
||||
* mode. When marked as {@link NotLoggedEvent}, it will not appear in detailed
|
||||
* bus logging (useful for very frequent events, such as UpdateEvent).
|
||||
* </p>
|
||||
* <p>
|
||||
* Events annotated as {@link NonConsumableEvent} will throw an exception upon
|
||||
* an attempt to consume them.
|
||||
* </p>
|
||||
* <p>
|
||||
* Default sending mode (if not changed by annotations) is <i>queued</i> with
|
||||
* zero delay.
|
||||
* </p>
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
* @param <HANDLER> handler type
|
||||
*/
|
||||
public abstract class BusEvent<HANDLER> {
|
||||
|
||||
private boolean consumed;
|
||||
private boolean served;
|
||||
|
||||
|
||||
/**
|
||||
* Ask handler to handle this message.
|
||||
*
|
||||
* @param handler handler instance
|
||||
*/
|
||||
protected abstract void handleBy(HANDLER handler);
|
||||
|
||||
|
||||
/**
|
||||
* Consume the event, so no other clients will receive it.
|
||||
*
|
||||
* @throws UnsupportedOperationException if the {@link NonConsumableEvent}
|
||||
* annotation is present.
|
||||
*/
|
||||
public final void consume()
|
||||
{
|
||||
if (consumed) throw new IllegalStateException("Already consumed.");
|
||||
|
||||
if (getClass().isAnnotationPresent(NonConsumableEvent.class)) {
|
||||
throw new UnsupportedOperationException("Not consumable.");
|
||||
}
|
||||
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deliver to a handler using the handleBy method.
|
||||
*
|
||||
* @param handler handler instance
|
||||
*/
|
||||
final void deliverTo(HANDLER handler)
|
||||
{
|
||||
handleBy(handler);
|
||||
|
||||
if (!served) {
|
||||
if (getClass().isAnnotationPresent(SingleReceiverEvent.class)) {
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
served = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the event is consumed. Consumed event is not served to other
|
||||
* clients.
|
||||
*
|
||||
* @return true if consumed
|
||||
*/
|
||||
public final boolean isConsumed()
|
||||
{
|
||||
return consumed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return true if the event was served to at least 1 client
|
||||
*/
|
||||
final boolean wasServed()
|
||||
{
|
||||
return served;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear "served" and "consumed" flags before dispatching.
|
||||
*/
|
||||
final void clearFlags()
|
||||
{
|
||||
served = false;
|
||||
consumed = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called after all clients have received the event.
|
||||
*
|
||||
* @param bus event bus instance
|
||||
*/
|
||||
public void onDispatchComplete(EventBus bus)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package mightypork.utils.eventbus;
|
||||
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import mightypork.utils.Reflect;
|
||||
import mightypork.utils.Support;
|
||||
import mightypork.utils.eventbus.clients.DelegatingClient;
|
||||
import mightypork.utils.eventbus.events.flags.DelayedEvent;
|
||||
import mightypork.utils.eventbus.events.flags.DirectEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NotLoggedEvent;
|
||||
import mightypork.utils.interfaces.Destroyable;
|
||||
import mightypork.utils.logging.Log;
|
||||
|
||||
|
||||
/**
|
||||
* An event bus, accommodating multiple EventChannels.<br>
|
||||
* Channel will be created when an event of type is first encountered.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
final public class EventBus implements Destroyable, BusAccess {
|
||||
|
||||
/**
|
||||
* Queued event holder
|
||||
*/
|
||||
private class DelayQueueEntry implements Delayed {
|
||||
|
||||
private final long due;
|
||||
private final BusEvent<?> evt;
|
||||
|
||||
|
||||
public DelayQueueEntry(double seconds, BusEvent<?> event)
|
||||
{
|
||||
super();
|
||||
this.due = System.currentTimeMillis() + (long) (seconds * 1000);
|
||||
this.evt = event;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o)
|
||||
{
|
||||
return Long.valueOf(getDelay(TimeUnit.MILLISECONDS)).compareTo(o.getDelay(TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit)
|
||||
{
|
||||
return unit.convert(due - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
|
||||
public BusEvent<?> getEvent()
|
||||
{
|
||||
return evt;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread handling queued events
|
||||
*/
|
||||
private class QueuePollingThread extends Thread {
|
||||
|
||||
public volatile boolean stopped = false;
|
||||
|
||||
|
||||
public QueuePollingThread()
|
||||
{
|
||||
super("Queue Polling Thread");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
DelayQueueEntry evt;
|
||||
|
||||
while (!stopped) {
|
||||
evt = null;
|
||||
|
||||
try {
|
||||
evt = sendQueue.take();
|
||||
} catch (final InterruptedException ignored) {
|
||||
//
|
||||
}
|
||||
|
||||
if (evt != null) {
|
||||
try {
|
||||
dispatch(evt.getEvent());
|
||||
} catch (final Throwable t) {
|
||||
Log.w(logMark + "Error while dispatching event: ", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final String logMark = "(bus) ";
|
||||
|
||||
|
||||
private static Class<?> getEventListenerClass(BusEvent<?> event)
|
||||
{
|
||||
// BEHOLD, MAGIC!
|
||||
|
||||
final Type evtc = event.getClass().getGenericSuperclass();
|
||||
|
||||
if (evtc instanceof ParameterizedType) {
|
||||
if (((ParameterizedType) evtc).getRawType() == BusEvent.class) {
|
||||
final Type[] types = ((ParameterizedType) evtc).getActualTypeArguments();
|
||||
for (final Type genericType : types) {
|
||||
return (Class<?>) genericType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("Could not detect event listener type.");
|
||||
}
|
||||
|
||||
/** Log detailed messages (debug) */
|
||||
public boolean detailedLogging = false;
|
||||
|
||||
/** Queue polling thread */
|
||||
private final QueuePollingThread busThread;
|
||||
|
||||
/** Registered clients */
|
||||
private final Set<Object> clients = Collections.newSetFromMap(new ConcurrentHashMap<Object, Boolean>());
|
||||
|
||||
/** Whether the bus was destroyed */
|
||||
private boolean dead = false;
|
||||
|
||||
/** Message channels */
|
||||
private final Set<EventChannel<?, ?>> channels = Collections.newSetFromMap(new ConcurrentHashMap<EventChannel<?, ?>, Boolean>());
|
||||
|
||||
/** Messages queued for delivery */
|
||||
private final DelayQueue<DelayQueueEntry> sendQueue = new DelayQueue<>();
|
||||
|
||||
|
||||
/**
|
||||
* Make a new bus and start it's queue thread.
|
||||
*/
|
||||
public EventBus()
|
||||
{
|
||||
busThread = new QueuePollingThread();
|
||||
busThread.setDaemon(true);
|
||||
busThread.start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Halt bus thread and reject any future events.
|
||||
*/
|
||||
@Override
|
||||
public void destroy()
|
||||
{
|
||||
assertLive();
|
||||
|
||||
busThread.stopped = true;
|
||||
dead = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send based on annotation
|
||||
*
|
||||
* @param event event
|
||||
*/
|
||||
public void send(BusEvent<?> event)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
final DelayedEvent adelay = Reflect.getAnnotation(event, DelayedEvent.class);
|
||||
if (adelay != null) {
|
||||
sendDelayed(event, adelay.delay());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Reflect.hasAnnotation(event, DirectEvent.class)) {
|
||||
sendDirect(event);
|
||||
return;
|
||||
}
|
||||
|
||||
sendQueued(event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add event to a queue
|
||||
*
|
||||
* @param event event
|
||||
*/
|
||||
public void sendQueued(BusEvent<?> event)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
sendDelayed(event, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add event to a queue, scheduled for given time.
|
||||
*
|
||||
* @param event event
|
||||
* @param delay delay before event is dispatched
|
||||
*/
|
||||
public void sendDelayed(BusEvent<?> event, double delay)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
final DelayQueueEntry dm = new DelayQueueEntry(delay, event);
|
||||
|
||||
if (shallLog(event)) {
|
||||
Log.f3(logMark + "Qu [" + Support.str(event) + "]" + (delay == 0 ? "" : (", delay: " + delay + "s")));
|
||||
}
|
||||
|
||||
sendQueue.add(dm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send immediately.<br>
|
||||
* Should be used for real-time events that require immediate response, such
|
||||
* as timing events.
|
||||
*
|
||||
* @param event event
|
||||
*/
|
||||
public void sendDirect(BusEvent<?> event)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
if (shallLog(event)) Log.f3(logMark + "Di [" + Support.str(event) + "]");
|
||||
|
||||
dispatch(event);
|
||||
}
|
||||
|
||||
|
||||
public void sendDirectToChildren(DelegatingClient delegatingClient, BusEvent<?> event)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
if (shallLog(event)) Log.f3(logMark + "Di->sub [" + Support.str(event) + "]");
|
||||
|
||||
doDispatch(delegatingClient.getChildClients(), event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connect a client to the bus. The client will be connected to all current
|
||||
* and future channels, until removed from the bus.
|
||||
*
|
||||
* @param client the client
|
||||
*/
|
||||
public void subscribe(Object client)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
if (client == null) return;
|
||||
|
||||
clients.add(client);
|
||||
|
||||
if (detailedLogging) Log.f3(logMark + "Client joined: " + Support.str(client));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect a client from the bus.
|
||||
*
|
||||
* @param client the client
|
||||
*/
|
||||
public void unsubscribe(Object client)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
clients.remove(client);
|
||||
|
||||
if (detailedLogging) Log.f3(logMark + "Client left: " + Support.str(client));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean addChannelForEvent(BusEvent<?> event)
|
||||
{
|
||||
try {
|
||||
if (detailedLogging) {
|
||||
Log.f3(logMark + "Setting up channel for new event type: " + Support.str(event.getClass()));
|
||||
}
|
||||
|
||||
final Class<?> listener = getEventListenerClass(event);
|
||||
final EventChannel<?, ?> ch = EventChannel.create(event.getClass(), listener);
|
||||
|
||||
if (ch.canBroadcast(event)) {
|
||||
|
||||
channels.add(ch);
|
||||
//channels.flush();
|
||||
|
||||
if (detailedLogging) {
|
||||
Log.f3(logMark + "Created new channel: " + Support.str(event.getClass()) + " -> " + Support.str(listener));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
Log.w(logMark + "Could not create channel for event " + Support.str(event.getClass()));
|
||||
}
|
||||
|
||||
} catch (final Throwable t) {
|
||||
Log.w(logMark + "Error while trying to add channel for event.", t);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure the bus is not destroyed.
|
||||
*
|
||||
* @throws IllegalStateException if the bus is dead.
|
||||
*/
|
||||
private void assertLive() throws IllegalStateException
|
||||
{
|
||||
if (dead) throw new IllegalStateException("EventBus is dead.");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send immediately.<br>
|
||||
* Should be used for real-time events that require immediate response, such
|
||||
* as timing events.
|
||||
*
|
||||
* @param event event
|
||||
*/
|
||||
private synchronized void dispatch(BusEvent<?> event)
|
||||
{
|
||||
assertLive();
|
||||
|
||||
doDispatch(clients, event);
|
||||
event.onDispatchComplete(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send to a set of clients
|
||||
*
|
||||
* @param clients clients
|
||||
* @param event event
|
||||
*/
|
||||
private synchronized void doDispatch(Collection<?> clients, BusEvent<?> event)
|
||||
{
|
||||
boolean accepted = false;
|
||||
|
||||
event.clearFlags();
|
||||
|
||||
for (int i = 0; i < 2; i++) { // two tries.
|
||||
|
||||
for (final EventChannel<?, ?> b : channels) {
|
||||
if (b.canBroadcast(event)) {
|
||||
accepted = true;
|
||||
b.broadcast(event, clients);
|
||||
}
|
||||
|
||||
if (event.isConsumed()) break;
|
||||
}
|
||||
|
||||
if (!accepted) if (addChannelForEvent(event)) continue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!accepted) Log.e(logMark + "Not accepted by any channel: " + Support.str(event));
|
||||
if (!event.wasServed() && shallLog(event)) Log.w(logMark + "Not delivered: " + Support.str(event));
|
||||
}
|
||||
|
||||
|
||||
private boolean shallLog(BusEvent<?> event)
|
||||
{
|
||||
if (!detailedLogging) return false;
|
||||
if (Reflect.hasAnnotation(event, NotLoggedEvent.class)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public EventBus getEventBus()
|
||||
{
|
||||
return this; // just for compatibility use-case
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package mightypork.utils.eventbus;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
import mightypork.utils.Reflect;
|
||||
import mightypork.utils.Support;
|
||||
import mightypork.utils.eventbus.clients.DelegatingClient;
|
||||
import mightypork.utils.eventbus.clients.ToggleableClient;
|
||||
import mightypork.utils.eventbus.events.flags.NonRejectableEvent;
|
||||
import mightypork.utils.logging.Log;
|
||||
|
||||
|
||||
/**
|
||||
* Event delivery channel, module of {@link EventBus}
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
* @param <EVENT> event type
|
||||
* @param <CLIENT> client (subscriber) type
|
||||
*/
|
||||
class EventChannel<EVENT extends BusEvent<CLIENT>, CLIENT> {
|
||||
|
||||
private final Class<CLIENT> clientClass;
|
||||
private final Class<EVENT> eventClass;
|
||||
|
||||
|
||||
/**
|
||||
* Create a channel
|
||||
*
|
||||
* @param eventClass event class
|
||||
* @param clientClass client class
|
||||
*/
|
||||
public EventChannel(Class<EVENT> eventClass, Class<CLIENT> clientClass)
|
||||
{
|
||||
|
||||
if (eventClass == null || clientClass == null) {
|
||||
throw new NullPointerException("Null Event or Client class.");
|
||||
}
|
||||
|
||||
this.clientClass = clientClass;
|
||||
this.eventClass = eventClass;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to broadcast a event.<br>
|
||||
* If event is of wrong type, <code>false</code> is returned.
|
||||
*
|
||||
* @param event a event to be sent
|
||||
* @param clients collection of clients
|
||||
*/
|
||||
public void broadcast(BusEvent<?> event, Collection<?> clients)
|
||||
{
|
||||
if (!canBroadcast(event)) return;
|
||||
|
||||
doBroadcast(eventClass.cast(event), clients, new HashSet<>());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the event
|
||||
*
|
||||
* @param event sent event
|
||||
* @param clients subscribing clients
|
||||
* @param processed clients already processed
|
||||
*/
|
||||
private void doBroadcast(final EVENT event, final Collection<?> clients, final Collection<Object> processed)
|
||||
{
|
||||
for (final Object client : clients) {
|
||||
|
||||
// exclude obvious non-clients
|
||||
if (!isClientValid(client)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// avoid executing more times
|
||||
if (processed.contains(client)) {
|
||||
Log.w(EventBus.logMark + "Client already served: " + Support.str(client));
|
||||
continue;
|
||||
}
|
||||
processed.add(client);
|
||||
|
||||
final boolean must_deliver = Reflect.hasAnnotation(event, NonRejectableEvent.class);
|
||||
|
||||
// opt-out
|
||||
if (client instanceof ToggleableClient) {
|
||||
if (!must_deliver && !((ToggleableClient) client).isListening()) continue;
|
||||
}
|
||||
|
||||
sendTo(client, event);
|
||||
|
||||
if (event.isConsumed()) return;
|
||||
|
||||
// pass on to delegated clients
|
||||
if (client instanceof DelegatingClient) {
|
||||
if (must_deliver || ((DelegatingClient) client).doesDelegate()) {
|
||||
|
||||
final Collection<?> children = ((DelegatingClient) client).getChildClients();
|
||||
|
||||
if (children != null && children.size() > 0) {
|
||||
doBroadcast(event, children, processed);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send an event to a client.
|
||||
*
|
||||
* @param client target client
|
||||
* @param event event to send
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void sendTo(Object client, EVENT event)
|
||||
{
|
||||
if (isClientOfChannelType(client)) {
|
||||
((BusEvent<CLIENT>) event).deliverTo((CLIENT) client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the given event can be broadcasted by this channel
|
||||
*
|
||||
* @param event event object
|
||||
* @return can be broadcasted
|
||||
*/
|
||||
public boolean canBroadcast(BusEvent<?> event)
|
||||
{
|
||||
return event != null && eventClass.isInstance(event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance for given types
|
||||
*
|
||||
* @param eventClass event class
|
||||
* @param clientClass client class
|
||||
* @return the broadcaster
|
||||
*/
|
||||
public static <F_EVENT extends BusEvent<F_CLIENT>, F_CLIENT> EventChannel<F_EVENT, F_CLIENT> create(Class<F_EVENT> eventClass, Class<F_CLIENT> clientClass)
|
||||
{
|
||||
return new EventChannel<>(eventClass, clientClass);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if client is of channel type
|
||||
*
|
||||
* @param client client
|
||||
* @return is of type
|
||||
*/
|
||||
private boolean isClientOfChannelType(Object client)
|
||||
{
|
||||
return clientClass.isInstance(client);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the channel is compatible with given
|
||||
*
|
||||
* @param client client
|
||||
* @return is supported
|
||||
*/
|
||||
public boolean isClientValid(Object client)
|
||||
{
|
||||
return isClientOfChannelType(client) || (client instanceof DelegatingClient);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
final int prime = 13;
|
||||
int result = 1;
|
||||
result = prime * result + ((clientClass == null) ? 0 : clientClass.hashCode());
|
||||
result = prime * result + ((eventClass == null) ? 0 : eventClass.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (!(obj instanceof EventChannel)) return false;
|
||||
final EventChannel<?, ?> other = (EventChannel<?, ?>) obj;
|
||||
if (clientClass == null) {
|
||||
if (other.clientClass != null) return false;
|
||||
} else if (!clientClass.equals(other.clientClass)) return false;
|
||||
if (eventClass == null) {
|
||||
if (other.eventClass != null) return false;
|
||||
} else if (!eventClass.equals(other.eventClass)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "{ " + Support.str(eventClass) + " => " + Support.str(clientClass) + " }";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import mightypork.utils.eventbus.BusAccess;
|
||||
import mightypork.utils.eventbus.EventBus;
|
||||
|
||||
|
||||
/**
|
||||
* Client that can be attached to the {@link EventBus}, or added as a child
|
||||
* client to another {@link DelegatingClient}
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public abstract class BusNode implements BusAccess, ClientHub {
|
||||
|
||||
private final BusAccess busAccess;
|
||||
|
||||
private final Set<Object> clients = new LinkedHashSet<>();
|
||||
private boolean listening = true;
|
||||
private boolean delegating = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param busAccess access to bus
|
||||
*/
|
||||
public BusNode(BusAccess busAccess)
|
||||
{
|
||||
this.busAccess = busAccess;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<Object> getChildClients()
|
||||
{
|
||||
return clients;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean doesDelegate()
|
||||
{
|
||||
return delegating;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isListening()
|
||||
{
|
||||
return listening;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a child subscriber to the {@link EventBus}.<br>
|
||||
*
|
||||
* @param client
|
||||
*/
|
||||
@Override
|
||||
public void addChildClient(Object client)
|
||||
{
|
||||
if (client instanceof RootBusNode) {
|
||||
throw new IllegalArgumentException("Cannot nest RootBusNode.");
|
||||
}
|
||||
|
||||
clients.add(client);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove a child subscriber
|
||||
*
|
||||
* @param client subscriber to remove
|
||||
*/
|
||||
@Override
|
||||
public void removeChildClient(Object client)
|
||||
{
|
||||
if (client != null) {
|
||||
clients.remove(client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether events should be received.
|
||||
*
|
||||
* @param listening receive events
|
||||
*/
|
||||
public void setListening(boolean listening)
|
||||
{
|
||||
this.listening = listening;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether events should be passed on to child nodes
|
||||
*
|
||||
* @param delegating
|
||||
*/
|
||||
public void setDelegating(boolean delegating)
|
||||
{
|
||||
this.delegating = delegating;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public EventBus getEventBus()
|
||||
{
|
||||
return busAccess.getEventBus();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import mightypork.utils.eventbus.EventBus;
|
||||
|
||||
|
||||
/**
|
||||
* Common methods for client hubs (ie delegating vlient implementations)
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public interface ClientHub extends DelegatingClient, ToggleableClient {
|
||||
|
||||
@Override
|
||||
public boolean doesDelegate();
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<Object> getChildClients();
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isListening();
|
||||
|
||||
|
||||
/**
|
||||
* Add a child subscriber to the {@link EventBus}.<br>
|
||||
*
|
||||
* @param client
|
||||
*/
|
||||
public void addChildClient(Object client);
|
||||
|
||||
|
||||
/**
|
||||
* Remove a child subscriber
|
||||
*
|
||||
* @param client subscriber to remove
|
||||
*/
|
||||
void removeChildClient(Object client);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
/**
|
||||
* Array-list with varargs constructor, intended to wrap fre clients for
|
||||
* delegating client.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public class ClientList extends ArrayList<Object> {
|
||||
|
||||
public ClientList(Object... clients)
|
||||
{
|
||||
for (final Object c : clients) {
|
||||
super.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
|
||||
/**
|
||||
* Client containing child clients. According to the contract, if the collection
|
||||
* of clients is ordered, the clients will be served in that order. In any case,
|
||||
* the {@link DelegatingClient} itself will be served beforehand.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public interface DelegatingClient {
|
||||
|
||||
/**
|
||||
* @return collection of child clients. Can not be null.
|
||||
*/
|
||||
public Collection<?> getChildClients();
|
||||
|
||||
|
||||
/**
|
||||
* @return true if delegating is active
|
||||
*/
|
||||
public boolean doesDelegate();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import mightypork.utils.interfaces.Enableable;
|
||||
|
||||
|
||||
/**
|
||||
* List of clients, that can be used as a delegating client.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public class DelegatingList extends ClientList implements DelegatingClient, Enableable {
|
||||
|
||||
private boolean enabled = true;
|
||||
|
||||
|
||||
public DelegatingList(Object... clients)
|
||||
{
|
||||
super(clients);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<?> getChildClients()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean doesDelegate()
|
||||
{
|
||||
return isEnabled();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean yes)
|
||||
{
|
||||
enabled = yes;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
import mightypork.utils.annotations.DefaultImpl;
|
||||
import mightypork.utils.eventbus.BusAccess;
|
||||
import mightypork.utils.interfaces.Destroyable;
|
||||
|
||||
|
||||
/**
|
||||
* Bus node that should be directly attached to the bus.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public abstract class RootBusNode extends BusNode implements Destroyable {
|
||||
|
||||
/**
|
||||
* @param busAccess access to bus
|
||||
*/
|
||||
public RootBusNode(BusAccess busAccess)
|
||||
{
|
||||
super(busAccess);
|
||||
|
||||
getEventBus().subscribe(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public final void destroy()
|
||||
{
|
||||
deinit();
|
||||
|
||||
getEventBus().unsubscribe(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deinitialize the subsystem<br>
|
||||
* (called during destruction)
|
||||
*/
|
||||
@DefaultImpl
|
||||
protected void deinit()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package mightypork.utils.eventbus.clients;
|
||||
|
||||
|
||||
/**
|
||||
* Client that can toggle receiving messages.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
public interface ToggleableClient {
|
||||
|
||||
/**
|
||||
* @return true if the client wants to receive messages
|
||||
*/
|
||||
public boolean isListening();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package mightypork.utils.eventbus.events;
|
||||
|
||||
|
||||
import mightypork.utils.eventbus.BusEvent;
|
||||
import mightypork.utils.eventbus.events.flags.DirectEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NonConsumableEvent;
|
||||
import mightypork.utils.interfaces.Destroyable;
|
||||
|
||||
|
||||
/**
|
||||
* Invoke destroy() method of all subscribers. Used to deinit a system.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@DirectEvent
|
||||
@NonConsumableEvent
|
||||
public class DestroyEvent extends BusEvent<Destroyable> {
|
||||
|
||||
@Override
|
||||
public void handleBy(Destroyable handler)
|
||||
{
|
||||
handler.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package mightypork.utils.eventbus.events;
|
||||
|
||||
|
||||
import mightypork.utils.eventbus.BusEvent;
|
||||
import mightypork.utils.eventbus.events.flags.DirectEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NonConsumableEvent;
|
||||
import mightypork.utils.eventbus.events.flags.NotLoggedEvent;
|
||||
import mightypork.utils.interfaces.Updateable;
|
||||
|
||||
|
||||
/**
|
||||
* Delta timing update event. Not logged.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@NotLoggedEvent
|
||||
@DirectEvent
|
||||
@NonConsumableEvent
|
||||
public class UpdateEvent extends BusEvent<Updateable> {
|
||||
|
||||
private final double deltaTime;
|
||||
|
||||
|
||||
/**
|
||||
* @param deltaTime time since last update (sec)
|
||||
*/
|
||||
public UpdateEvent(double deltaTime)
|
||||
{
|
||||
this.deltaTime = deltaTime;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleBy(Updateable handler)
|
||||
{
|
||||
handler.update(deltaTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
/**
|
||||
* Event that should be queued with given delay (default: 0);
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
@Documented
|
||||
public @interface DelayedEvent {
|
||||
|
||||
/**
|
||||
* @return event dispatch delay [seconds]
|
||||
*/
|
||||
double delay() default 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
/**
|
||||
* Event that should not be queued.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
@Documented
|
||||
public @interface DirectEvent {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Event that cannot be consumed
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface NonConsumableEvent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Event that is forcibly delivered to all clients (bypass Toggleable etc)
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface NonRejectableEvent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
/**
|
||||
* Event that's not worth logging, unless there was an error with it.<br>
|
||||
* Useful for common events that would otherwise clutter the log.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
@Documented
|
||||
public @interface NotLoggedEvent {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mightypork.utils.eventbus.events.flags;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
/**
|
||||
* Handled only by the first client, then discarded.
|
||||
*
|
||||
* @author Ondřej Hruška (MightyPork)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
@Documented
|
||||
public @interface SingleReceiverEvent {}
|
||||
Reference in New Issue
Block a user