diff --git a/module-fx/pom.xml b/module-fx/pom.xml index 8dd8dad..7f5ff1b 100644 --- a/module-fx/pom.xml +++ b/module-fx/pom.xml @@ -58,6 +58,25 @@ org.slf4j slf4j-api + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.assertj + assertj-core + 3.11.1 + test + + + + org.mockito + mockito-core + test + diff --git a/module-fx/src/main/java/place/sita/modulefx/BadApiUsageException.java b/module-fx/src/main/java/place/sita/modulefx/BadApiUsageException.java new file mode 100644 index 0000000..43f3d86 --- /dev/null +++ b/module-fx/src/main/java/place/sita/modulefx/BadApiUsageException.java @@ -0,0 +1,19 @@ +package place.sita.modulefx; + +public class BadApiUsageException extends RuntimeException { + + public BadApiUsageException() { + } + + public BadApiUsageException(String message) { + super(message); + } + + public BadApiUsageException(String message, Throwable cause) { + super(message, cause); + } + + public BadApiUsageException(Throwable cause) { + super(cause); + } +} diff --git a/module-fx/src/main/java/place/sita/modulefx/vtg/MessageListener.java b/module-fx/src/main/java/place/sita/modulefx/vtg/MessageListener.java new file mode 100644 index 0000000..2bd7ad3 --- /dev/null +++ b/module-fx/src/main/java/place/sita/modulefx/vtg/MessageListener.java @@ -0,0 +1,7 @@ +package place.sita.modulefx.vtg; + +public interface MessageListener { + + void receive(Object message); + +} diff --git a/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroup.java b/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroup.java new file mode 100644 index 0000000..afb16a5 --- /dev/null +++ b/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroup.java @@ -0,0 +1,65 @@ +package place.sita.modulefx.vtg; + +import place.sita.modulefx.BadApiUsageException; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class VirtualTreeGroup { + + private final UUID id = UUID.randomUUID(); + private VirtualTreeGroup parent; + private final List children = new ArrayList<>(); + private final List group = new ArrayList<>(); + + public UUID id() { + return id; + } + + public void addChild(VirtualTreeGroup node) { + if (node.parent != null) { + throw new BadApiUsageException("Node already has a parent"); + } + node.parent = this; + children.add(node); + } + + public void removeChild(UUID id) { + VirtualTreeGroup child = children.stream().filter(n -> n.id().equals(id)).findFirst().orElse(null); + + if (child == null) { + throw new BadApiUsageException("Not a child of this group"); + } + + child.parent = null; + children.remove(child); + } + + /** + * Passes message to all other connected nodes + */ + public void message(UUID senderId, Object message) { + for (VirtualTreeGroupElement element : group) { + if (element.getId().equals(senderId)) { + continue; + } + element.receive(message); + } + + if (parent != null && !parent.id().equals(senderId)) { + parent.message(id, message); + } + + for (VirtualTreeGroup child : children) { + if (!child.id().equals(senderId)) { + child.message(id, message); + } + } + } + + public void addElement(VirtualTreeGroupElement element) { + group.add(element); + } + +} diff --git a/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroupElement.java b/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroupElement.java new file mode 100644 index 0000000..aff195f --- /dev/null +++ b/module-fx/src/main/java/place/sita/modulefx/vtg/VirtualTreeGroupElement.java @@ -0,0 +1,25 @@ +package place.sita.modulefx.vtg; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class VirtualTreeGroupElement { + + private final UUID id = UUID.randomUUID(); + private final List listeners = new ArrayList<>(); + + public UUID getId() { + return id; + } + + public void addListener(MessageListener listener) { + listeners.add(listener); + } + + public void receive(Object message) { + for (MessageListener listener : listeners) { + listener.receive(message); + } + } +} diff --git a/module-fx/src/test/java/place/sita/modulefx/vtg/VirtualTreeGroupTest.java b/module-fx/src/test/java/place/sita/modulefx/vtg/VirtualTreeGroupTest.java new file mode 100644 index 0000000..f043a88 --- /dev/null +++ b/module-fx/src/test/java/place/sita/modulefx/vtg/VirtualTreeGroupTest.java @@ -0,0 +1,86 @@ +package place.sita.modulefx.vtg; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import place.sita.modulefx.BadApiUsageException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; + +public class VirtualTreeGroupTest { + + @Test + public void shouldReportAddingChildTwice() { + // given + VirtualTreeGroup group = new VirtualTreeGroup(); + VirtualTreeGroup child = new VirtualTreeGroup(); + group.addChild(child); + + // when + assertThatThrownBy(() -> group.addChild(child)) + // then + .isInstanceOf(BadApiUsageException.class) + .hasMessage("Node already has a parent"); + } + + @Test + public void shouldPassMessageToChildrenParentAndOtherElementsButNotSelf() { + // given + VirtualTreeGroup groupWithSelf = new VirtualTreeGroup(); + VirtualTreeGroup parent = new VirtualTreeGroup(); + VirtualTreeGroup child = new VirtualTreeGroup(); + parent.addChild(groupWithSelf); + groupWithSelf.addChild(child); + + MessageListener listenerInParent = Mockito.mock(MessageListener.class); + MessageListener otherListenerInMiddle = Mockito.mock(MessageListener.class); + MessageListener selfListenerInMiddle = Mockito.mock(MessageListener.class); + MessageListener listenerInChild = Mockito.mock(MessageListener.class); + + VirtualTreeGroupElement parentElement = new VirtualTreeGroupElement(); + parentElement.addListener(listenerInParent); + parent.addElement(parentElement); + + VirtualTreeGroupElement otherElementInMiddle = new VirtualTreeGroupElement(); + otherElementInMiddle.addListener(otherListenerInMiddle); + groupWithSelf.addElement(otherElementInMiddle); + + VirtualTreeGroupElement selfElementInMiddle = new VirtualTreeGroupElement(); + selfElementInMiddle.addListener(selfListenerInMiddle); + groupWithSelf.addElement(selfElementInMiddle); + + VirtualTreeGroupElement childElement = new VirtualTreeGroupElement(); + childElement.addListener(listenerInChild); + child.addElement(childElement); + + // when + groupWithSelf.message(selfElementInMiddle.getId(), "message"); + + // then + Mockito.verify(listenerInParent).receive("message"); + Mockito.verify(otherListenerInMiddle).receive("message"); + Mockito.verify(selfListenerInMiddle, Mockito.never()).receive(anyString()); + Mockito.verify(listenerInChild).receive("message"); + } + + @Test + public void shouldNotMessageRemovedChild() { + // given + VirtualTreeGroup group = new VirtualTreeGroup(); + VirtualTreeGroup child = new VirtualTreeGroup(); + group.addChild(child); + + MessageListener listener = Mockito.mock(MessageListener.class); + VirtualTreeGroupElement element = new VirtualTreeGroupElement(); + element.addListener(listener); + child.addElement(element); + + // when + group.removeChild(child.id()); + group.message(element.getId(), "message"); + + // then + Mockito.verify(listener, Mockito.never()).receive("message"); + } + +}