From cff157a0cd7af13f6b9b34ce1b62f6955a84c055 Mon Sep 17 00:00:00 2001 From: Jakob Alexander Eichler Date: Tue, 3 Jun 2025 12:14:43 +0200 Subject: [PATCH] Update ObservableEventDispatcher.java, ProfilingEventDispatcher.java, and 5 more files... --- eventflow/ObservableEventDispatcher.java | 280 ++++++++++++++++++ eventflow/ProfilingEventDispatcher.java | 29 ++ eventflow/event/BasicEvent.java | 10 + eventflow/event/ObservableEvent.java | 6 + eventflow/event/ObserverSortingEvent.java | 81 +++++ eventflow/listener/BasicEventListener.java | 5 + eventflow/listener/SortableEventListener.java | 17 ++ 7 files changed, 428 insertions(+) create mode 100755 eventflow/ObservableEventDispatcher.java create mode 100755 eventflow/ProfilingEventDispatcher.java create mode 100755 eventflow/event/BasicEvent.java create mode 100755 eventflow/event/ObservableEvent.java create mode 100755 eventflow/event/ObserverSortingEvent.java create mode 100755 eventflow/listener/BasicEventListener.java create mode 100755 eventflow/listener/SortableEventListener.java diff --git a/eventflow/ObservableEventDispatcher.java b/eventflow/ObservableEventDispatcher.java new file mode 100755 index 0000000..c675cc2 --- /dev/null +++ b/eventflow/ObservableEventDispatcher.java @@ -0,0 +1,280 @@ +package de.midlane_illaoi.eventflow; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +import de.midlane_illaoi.eventflow.event.ObservableEvent; +import de.midlane_illaoi.eventflow.event.ObserverSortingEvent; + +public class ObservableEventDispatcher +{ + private Map>, Collection> observersMap; + + private List> queuedModifications; + + class QueuedModification{ + private Class> eventType; + private T observer; + private boolean isSubscription; + + public QueuedModification(Class> eventType, T observer, boolean isSubscription) { + super(); + this.eventType = eventType; + this.observer = observer; + this.isSubscription = isSubscription; + } + + public Class> getEventType() { + return eventType; + } + + public T getObserver() { + return observer; + } + + public boolean isSubscription() { + return isSubscription; + } + } + + /** + * This variable serves as a recursion counter for nested event dispatching. + * */ + private int isDispatching; + + public ObservableEventDispatcher() { + isDispatching = 0; + observersMap = new HashMap<>(); + queuedModifications = new LinkedList>(); + } + + /** + * Currently this dispacther favors performance over functionality. + * It is currently not possible to change active listeners with a listener for the currently dispatched event. + * Anyways it is possible to change subscribed listener with a listener. + * The changes will become active after the dispatching of the currently dispatched event and dispatching of all events that caused the currently dispatched event stops. + * */ + public void subscribe(Class> eventType, T observer) { + + Collection computeIfAbsent; + if( isDispatching > 0 ) { + queuedModifications.add( new QueuedModification(eventType, observer, true) ); + }else { + computeIfAbsent = getOrCreateListenerSet(eventType); + computeIfAbsent.add(observer); + } + + } + + + /** + * Currently this dispacther favors performance over functionality. + * It is currently not possible to change active listeners with a listener for the currently dispatched event. + * Anyways it is possible to change subscribed listener with a listener. + * The changes will become active after the dispatching of the currently dispatched event stops. + * + * @return true if the listener was currently subscribed and removed. false other wise. + * false is also returned in case the dispatcher is currently dispatching and queues the removal. + * */ + public boolean unsubscribe(Class> eventType, T observer) { + + Collection computeIfAbsent; + if( isDispatching > 0 ) { + queuedModifications.add( new QueuedModification(eventType, observer, false) ); + return false; + }else { + computeIfAbsent = getOrCreateListenerSet(eventType); + return computeIfAbsent.remove(observer); + } + + + } + + + @SuppressWarnings({ "unchecked"}) + private Collection getOrCreateListenerSet(Class> eventType) + { + + Collection computeIfAbsent = observersMap.computeIfAbsent(eventType, key -> { + if( ObserverSortingEvent.class.isAssignableFrom(eventType) ) { + //ObserverSortingEvent eventInstance = getSortableEventTypeInstance((Class) eventType); + ObserverSortingEvent eventInstance = getSortableEventTypeInstance(castObservableEventClassToObservableEventClass(eventType)); + if( eventInstance.isComparatorUsedForAllEventsOfType() ) { + Comparator observerComparator = (Comparator) eventInstance.getObserverComparator(); + return new TreeSet( observerComparator ); + } + } + return new ArrayList(); + }); + + return (Collection) computeIfAbsent; + } + + @SuppressWarnings("unchecked") + private Class castObservableEventClassToObservableEventClass(Class> eventType){ + return (Class) eventType; + } + + private T getSortableEventTypeInstance( Class eventType ) + { + Constructor constructor = null; + Boolean accessibility = null; + try { + // Get the (private) default constructor + constructor = eventType.getDeclaredConstructor(); + accessibility = constructor.isAccessible(); + + // Set the accessibility to true + constructor.setAccessible(true); + + // Create an instance using the private constructor + + return (T) constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + String errorMessage = String.format("Cannot subscribe to events of type: %s. Events that implement the %s interface must implement a (private) default constructor.", eventType.getName(), ObserverSortingEvent.class.getName()); + throw new RuntimeException( errorMessage, e); + }finally { + if( accessibility != null && constructor != null) { + constructor.setAccessible(accessibility); + } + } + } + + + + public void dispatchEvent(ObservableEvent event) + { + isDispatching++; + try { + List> eventTypes = getAllEventSuperclasses(event.getClass()); + + + for (Class eventType : eventTypes) { + if(!observersMap.containsKey(eventType)) { + continue; + } + + //List observers = (List) observersMap.get(eventType); + + + /* + * Make a copy of the observers list. + * + * Observers can now unsubscribe while being notified. + * + * Alternative solution: Queuing Unsubscribe Calls: + * + * + * Pros: + * Lower cpu usage + * Lower Memory Usage: There's no need to create copies of observer lists, so memory usage remains low. + * Deferred Unsubscription: Unsubscribe calls are deferred until after event dispatch, allowing observers to continue receiving events during the current dispatch cycle. + * + * Cons: + * Complexity: Managing a queue of unsubscribe commands adds complexity to the implementation. + * Potential Delay: Unsubscribed observers may receive events during the current dispatch cycle. + * + */ + /* + * + * @SuppressWarnings("unchecked") + * List observers = new ArrayList<>( (Set) observersMap.get(eventType) ); + */ + + @SuppressWarnings("unchecked") + Collection observers = (Collection) observersMap.get(eventType); + + + /* + * currently for a ObserverSortingEvent all listeners + * get sorted for every dispatched event. + * + * This may slow down execution. + * An alternative is to keep the observers for such event types + * in a sorted list + * */ + if (event instanceof ObserverSortingEvent) { + ObserverSortingEvent sortingEvent = (ObserverSortingEvent) event; + + if( !sortingEvent.isComparatorUsedForAllEventsOfType() ) { + observers = new ArrayList<>( observers ); + @SuppressWarnings("unchecked") + Comparator observerComparator = (Comparator) sortingEvent.getObserverComparator(); + ((ArrayList)observers).sort( observerComparator ); + } + } + + observers.forEach(observer -> { + event.accept(observer); + }); + } + + /* + Class> eventType = getEventType(event); + @SuppressWarnings("unchecked") + List observers = (List) observersMap.computeIfAbsent(eventType, key -> new ArrayList()); + observers.forEach(observer -> { + event.accept(observer); + }); + */ + }catch (Exception e){ + throw e; + } + finally { + isDispatching--; + } + + + /* + * code changed at 15.07.2024 + * old code did not check for + * if( isDispatching == 0 ) { + * */ + if( isDispatching == 0 ) { + for( QueuedModification queuedModification : queuedModifications ) { + if( queuedModification.isSubscription() ) { + subscribeHelper(queuedModification); + }else { + unsubscribeHelper(queuedModification); + } + } + queuedModifications.clear(); + } + + } + + + private void subscribeHelper(QueuedModification queuedModification) { + subscribe(queuedModification.getEventType(), queuedModification.getObserver()); + } + + private void unsubscribeHelper(QueuedModification queuedModification) { + unsubscribe(queuedModification.getEventType(), queuedModification.getObserver()); + } + + private List> getAllEventSuperclasses(Class eventType) + { + List> superclasses = new ArrayList<>(); + while (eventType != null && ObservableEvent.class.isAssignableFrom(eventType)) { + superclasses.add(eventType); + eventType = eventType.getSuperclass(); + } + return superclasses; + } + + @SuppressWarnings({ "unchecked", "unused" }) + private Class> getEventType(ObservableEvent event) { + @SuppressWarnings("rawtypes") + Class eventClass = event.getClass(); + return (Class>) eventClass; + } +} diff --git a/eventflow/ProfilingEventDispatcher.java b/eventflow/ProfilingEventDispatcher.java new file mode 100755 index 0000000..def991e --- /dev/null +++ b/eventflow/ProfilingEventDispatcher.java @@ -0,0 +1,29 @@ +package de.midlane_illaoi.eventflow; + +import java.util.HashMap; +import java.util.Map; + +import de.midlane_illaoi.eventflow.event.ObservableEvent; + +public class ProfilingEventDispatcher extends ObservableEventDispatcher{ + + private Map, Integer> eventTypeCounter; + + public ProfilingEventDispatcher() { + eventTypeCounter = new HashMap, Integer>(); + } + + @Override + public void dispatchEvent(ObservableEvent event) { + super.dispatchEvent(event); + int count = eventTypeCounter.computeIfAbsent(event.getClass(), c -> 0); + eventTypeCounter.put(event.getClass(), count+1); + } + + public void printEventsCount() { + for (Map.Entry, Integer> entry : eventTypeCounter.entrySet()) { + System.out.println("Event type: " + entry.getKey().getSimpleName() + ",\t\t\t\tEvents count:: " + entry.getValue()); + } + } + +} diff --git a/eventflow/event/BasicEvent.java b/eventflow/event/BasicEvent.java new file mode 100755 index 0000000..6d58769 --- /dev/null +++ b/eventflow/event/BasicEvent.java @@ -0,0 +1,10 @@ +package de.midlane_illaoi.eventflow.event; + +import de.midlane_illaoi.eventflow.listener.BasicEventListener; + +public abstract class BasicEvent> implements ObservableEvent> { + @SuppressWarnings("unchecked") + public void accept(BasicEventListener eventlistener) { + eventlistener.onEvent((T) this); + } +} diff --git a/eventflow/event/ObservableEvent.java b/eventflow/event/ObservableEvent.java new file mode 100755 index 0000000..e2f2c03 --- /dev/null +++ b/eventflow/event/ObservableEvent.java @@ -0,0 +1,6 @@ +package de.midlane_illaoi.eventflow.event; + +public interface ObservableEvent +{ + public void accept(T eventlistener); +} \ No newline at end of file diff --git a/eventflow/event/ObserverSortingEvent.java b/eventflow/event/ObserverSortingEvent.java new file mode 100755 index 0000000..6b9ca5e --- /dev/null +++ b/eventflow/event/ObserverSortingEvent.java @@ -0,0 +1,81 @@ +package de.midlane_illaoi.eventflow.event; + +import java.util.Comparator; + +import de.midlane_illaoi.eventflow.listener.SortableEventListener; + +public interface ObserverSortingEvent +{ + + /** + * TODO write library to handle bitflags + * refactor this code & AbstractBuff + public static final int ON_UNCOMPARABLE_LISTENERS_THROW_EXCEPTION = 1; + public static final int EXECUTE_UNCOMPARABLE_LISTENERS_FIRST = 2; + public static final int EXECUTE_UNCOMPARABLE_LISTENERS_LAST = 4; + public static final int COMPARE_UNCAMPARABLE_LISTENERS_BY_HASHCODE = 8; + */ + + + enum SortingMode { + ON_UNCOMPARABLE_LISTENERS_THROW_EXCEPTION, + EXECUTE_UNCOMPARABLE_LISTENERS_FIRST, + EXECUTE_UNCOMPARABLE_LISTENERS_LAST, + } + + + public Comparator getObserverComparator(); + + default public boolean isComparatorUsedForAllEventsOfType() { return true; } + + /** + * The comparator compares all objects, + * but treats every object that does + * not implement the SortableEventListener interface as equal + * */ + default Comparator createObserverComparator(SortingMode sortingMode){ + return new Comparator() { + @Override + public int compare(Object o1, Object o2) { + boolean isO1Comparable = o1 instanceof SortableEventListener; + boolean isO2Comparable = o2 instanceof SortableEventListener; + + if( sortingMode == SortingMode.ON_UNCOMPARABLE_LISTENERS_THROW_EXCEPTION && isO1Comparable == isO2Comparable ) { + throw new RuntimeException("All listeners for this event must implement the SortableEventListener interface"); + } + + if( isO1Comparable && isO2Comparable ) { + SortableEventListener o1Sortable = (SortableEventListener) o1; + SortableEventListener o2Sortable = (SortableEventListener) o2; + /* + * sort by priority in an ascending order + * */ + return Integer.compare(o2Sortable.getPriority(), o1Sortable.getPriority()); + }else if( !( isO1Comparable || isO2Comparable) ) { + /*two uncomparable objects are treated as equal*/ + /* using has code here for comparison instead of returning 0 + * slows down sorting but allows to use this comparator for a tree set + * */ + return Integer.compare(o1.hashCode(), o2.hashCode()); + }else if(isO1Comparable){ + /* + * o1 is comparable but o2 is not + * */ + if(sortingMode == SortingMode.EXECUTE_UNCOMPARABLE_LISTENERS_FIRST) { + return -1; + }else /*sortingMode == SortingMode.EXECUTE_UNCOMPARABLE_LISTENERS_LAST*/{ + return -1; + } + + }else { + /*o2 is comparable but o1 is not*/ + if(sortingMode == SortingMode.EXECUTE_UNCOMPARABLE_LISTENERS_FIRST) { + return 1; + }else /*sortingMode == SortingMode.EXECUTE_UNCOMPARABLE_LISTENERS_LAST*/{ + return -1; + } + } + } + }; + } +} diff --git a/eventflow/listener/BasicEventListener.java b/eventflow/listener/BasicEventListener.java new file mode 100755 index 0000000..93ef03d --- /dev/null +++ b/eventflow/listener/BasicEventListener.java @@ -0,0 +1,5 @@ +package de.midlane_illaoi.eventflow.listener; + +public interface BasicEventListener { + public void onEvent(T event); +} \ No newline at end of file diff --git a/eventflow/listener/SortableEventListener.java b/eventflow/listener/SortableEventListener.java new file mode 100755 index 0000000..407c099 --- /dev/null +++ b/eventflow/listener/SortableEventListener.java @@ -0,0 +1,17 @@ +package de.midlane_illaoi.eventflow.listener; + +import de.midlane_illaoi.eventflow.ObservableEventDispatcher; +import de.midlane_illaoi.eventflow.event.ObserverSortingEvent; + +/** + * This interfaces is used by the {@link ObserverSortingEvent} to sort event listeners. + * A higher priority value will lead to an earlier execution when using the {@link ObservableEventDispatcher} + * */ +public interface SortableEventListener +{ + /** + * a higher priority indicates an earlier execution. + * negative priority values are allowed + * */ + public int getPriority(); +}