Skip to content

Commit

Permalink
Add listenAll method to EventManager.
Browse files Browse the repository at this point in the history
  • Loading branch information
LambdAurora committed Oct 25, 2023
1 parent f8263b2 commit 44cf105
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
Expand All @@ -43,9 +45,11 @@
*/
public final class EventManager<I extends Comparable<? super I>> {
private final I defaultPhaseId;
private final Function<String, I> phaseIdParser;

public EventManager(@NotNull I defaultPhaseId) {
public EventManager(@NotNull I defaultPhaseId, @NotNull Function<String, I> phaseIdParser) {
this.defaultPhaseId = defaultPhaseId;
this.phaseIdParser = phaseIdParser;
}

/**
Expand Down Expand Up @@ -249,6 +253,47 @@ public EventManager(@NotNull I defaultPhaseId) {
return event;
}

/**
* Registers the listener to the specified events.
* <p>
* The registration of the listener will be rejected if one of the listed event involves generics in its listener type,
* as checking for valid registration is too expensive, please use the regular {@link Event#register(Object)} method
* for those as those checks will be delegated to the Java compiler.
*
* @param listener the listener object
* @param events the events to listen
* @throws IllegalArgumentException if the listener doesn't listen one of the events to listen, or if no events were specified
* @see Event#register(Object)
* @see Event#register(Comparable, Object)
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@SafeVarargs
public final void listenAll(Object listener, Event<I, ?>... events) {
if (events.length == 0) {
throw new IllegalArgumentException("Tried to register a listener for an empty event list.");
}

var listenedPhases = this.getListenedPhases(listener.getClass());

// Check whether we actually can register stuff. We only commit the registration if all events can.
for (var event : events) {
if (!event.getType().isAssignableFrom(listener.getClass())) {
throw new IllegalArgumentException("Given object " + listener + " is not a listener of event " + event);
}

if (event.getType().getTypeParameters().length > 0) {
throw new IllegalArgumentException("Cannot register a listener for the event " + event + " which is using generic parameters with listenAll.");
}

listenedPhases.putIfAbsent(event.getType(), this.defaultPhaseId);
}

// We can register, so we do!
for (var event : events) {
((Event) event).register(listenedPhases.get(event.getType()), listener);
}
}

/**
* {@return the default phase identifier of all the events created by this event manager}
*/
Expand All @@ -257,6 +302,22 @@ public EventManager(@NotNull I defaultPhaseId) {
return this.defaultPhaseId;
}

private Map<Class<?>, I> getListenedPhases(Class<?> listenerClass) {
var map = new HashMap<Class<?>, I>();

for (var annotation : listenerClass.getAnnotations()) {
if (annotation instanceof ListenerPhase phase) {
map.put(phase.callbackTarget(), this.phaseIdParser.apply(phase.value()));
} else if (annotation instanceof ListenerPhases phases) {
for (var phase : phases.value()) {
map.put(phase.callbackTarget(), this.phaseIdParser.apply(phase.value()));
}
}
}

return map;
}

private void ensureContainsDefaultPhase(I[] defaultPhases) {
for (var id : defaultPhases) {
if (id.equals(this.defaultPhaseId)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 Yumi Project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.yumi.commons.event;


import java.lang.annotation.*;

/**
* Annotates a specific callback in a listener a specific phase to listen.
*
* @see Event#addPhaseOrdering(Comparable, Comparable)
* @see Event#register(Comparable, Object)
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(ListenerPhases.class)
public @interface ListenerPhase {
/**
* {@return the targeted callback interface}
*/
Class<?> callbackTarget();

/**
* {@return the identifier of the phase to listen}
*/
String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2023 Yumi Project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.yumi.commons.event;


import java.lang.annotation.*;

/**
* Represents the container type of {@link ListenerPhase} to make it repeatable.
*
* @see ListenerPhase
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ListenerPhases {
ListenerPhase[] value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright 2023 Yumi Project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.yumi.commons.event.test;

import dev.yumi.commons.collections.YumiCollections;
import dev.yumi.commons.event.EventManager;
import dev.yumi.commons.event.ListenerPhase;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.function.Function;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class EventListenAllTest {
@Test
public void testListenAllRejectNoEvents() {
var events = new EventManager<>("default", Function.identity());

assertThrows(IllegalArgumentException.class, () -> {
events.listenAll(new Listener(new ExecutionTester()));
});
}

@Test
public void testListenAllRejectMissingEvents() {
var events = new EventManager<>("default", Function.identity());
var testEvent = events.create(TestCallback.class);
var testFilterEvent = events.create(FilterTestCallback.class);

assertThrows(IllegalArgumentException.class, () -> {
events.listenAll(new Object(), testEvent, testFilterEvent);
});
}

@SuppressWarnings("unchecked")
@Test
public void testListenAllRejectGenerics() {
var events = new EventManager<>("default", Function.identity());
var testGenericEvent = events.create(Generic.class, listeners -> arg -> {
for (var listener : listeners) {
listener.invoke(arg);
}
});
var generic = new Generic<String>() {
@Override
public void invoke(String arg) {
}
};

assertThrows(IllegalArgumentException.class, () -> {
events.listenAll(generic, testGenericEvent);
});
}

@Test
public void testListenAll() {
var events = new EventManager<>("default", Function.identity());
var testEvent = events.create(TestCallback.class);
var testFilterEvent = events.create(FilterTestCallback.class);
var tester = new ExecutionTester();

events.listenAll(new Listener(tester), testEvent, testFilterEvent);

testEvent.invoker().call("Test");
tester.assertCalled(1);

testFilterEvent.invoker().filter("Yippee");
tester.assertCalled(2);
}

@Test
public void testListenAllPhases() {
var events = new EventManager<>("default", Function.identity());
var tester = new ExecutionTester();

YumiCollections.forAllPermutations(
List.of(
List.of(new PhaseListenerEarly(tester)),
List.of(new PhaseListenerDefault(tester), new PhaseListenerExplicitDefault(tester)),
List.of(new PhaseListenerLate(tester))
),
listeners -> {
var testEvent = events.createWithPhases(TestCallback.class, "early", "default", "late");
var filterEvent = events.createWithPhases(FilterTestCallback.class, "early", "default", "late");

for (var phaseListeners : listeners) {
for (var listener : phaseListeners) {
events.listenAll(listener, testEvent, filterEvent);
}
}

tester.reset();
testEvent.invoker().call("Test");
tester.assertCalled(4);
tester.reset();
assertTrue(filterEvent.invoker().filter("Yippee"));
tester.assertCalled(3);
});
}

interface Generic<T> {
void invoke(T arg);
}

@ListenerPhase(callbackTarget = TestCallback.class, value = "early")
@ListenerPhase(callbackTarget = FilterTestCallback.class, value = "late")
record PhaseListenerEarly(ExecutionTester tester) implements TestCallback, FilterTestCallback {
@Override
public void call(String text) {
this.tester.assertOrder(0);
}

@Override
public boolean filter(String text) {
throw new IllegalStateException("The filter callback should have already returned a value.");
}
}

record PhaseListenerDefault(ExecutionTester tester) implements TestCallback, FilterTestCallback {
@Override
public void call(String text) {
this.tester.assertOrder(1);
}

@Override
public boolean filter(String text) {
this.tester.assertOrder(1);
return false;
}
}

@ListenerPhase(callbackTarget = TestCallback.class, value = "default")
@ListenerPhase(callbackTarget = FilterTestCallback.class, value = "default")
record PhaseListenerExplicitDefault(ExecutionTester tester) implements TestCallback, FilterTestCallback {
@Override
public void call(String text) {
this.tester.assertOrder(2);
}

@Override
public boolean filter(String text) {
this.tester.assertOrder(2);
return true;
}
}

@ListenerPhase(callbackTarget = TestCallback.class, value = "late")
@ListenerPhase(callbackTarget = FilterTestCallback.class, value = "early")
record PhaseListenerLate(ExecutionTester tester) implements TestCallback, FilterTestCallback {
@Override
public void call(String text) {
this.tester.assertOrder(3);
}

@Override
public boolean filter(String text) {
this.tester.assertOrder(0);
return false;
}
}

record Listener(ExecutionTester tester) implements TestCallback, FilterTestCallback {

@Override
public void call(String text) {
this.tester.assertOrder(0);
}

@Override
public boolean filter(String text) {
this.tester.assertOrder(1);
return false;
}
}
}
Loading

0 comments on commit 44cf105

Please sign in to comment.