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");
+ }
+
+}