diff --git a/node.iml b/node.iml index a66a2bf..b2d3a3d 100644 --- a/node.iml +++ b/node.iml @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 32577de..c488538 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,10 @@ 0.6.0 + + 3.4.1 + + https://github.com/kemitix/node/issues GitHub Issues @@ -48,5 +52,11 @@ 1.3 test + + org.assertj + assertj-core + ${assertj.version} + test + diff --git a/src/main/java/net/kemitix/node/Node.java b/src/main/java/net/kemitix/node/Node.java index 002bead..a8dccdf 100644 --- a/src/main/java/net/kemitix/node/Node.java +++ b/src/main/java/net/kemitix/node/Node.java @@ -13,6 +13,20 @@ import java.util.Set; */ public interface Node { + /** + * Fetch the name of the node. + * + * @return the name of the node + */ + String getName(); + + /** + * Sets the explicit name for a node. + * + * @param name the new name + */ + void setName(String name); + /** * Fetch the data held within the node. * @@ -20,6 +34,13 @@ public interface Node { */ T getData(); + /** + * Returns true if the node is empty (has no data). + * + * @return true is data is null + */ + boolean isEmpty(); + /** * Fetch the parent node. *

@@ -30,6 +51,13 @@ public interface Node { */ Node getParent(); + /** + * Make the current node a direct child of the parent. + * + * @param parent the new parent node + */ + void setParent(final Node parent); + /** * Fetches the child nodes. * @@ -89,13 +117,6 @@ public interface Node { */ boolean isChildOf(final Node node); - /** - * Make the current node a direct child of the parent. - * - * @param parent the new parent node - */ - void setParent(final Node parent); - /** * Walks the node tree using the path to select each child. * @@ -105,4 +126,59 @@ public interface Node { */ Optional> walkTree(final List path); + /** + * Places the node in the tree under by the path. Intervening empty + * nodes are created as needed. + * + * @param node the node to place + * @param path the path to contain the new node + */ + void placeNodeIn(Node node, String... path); + + /** + * Searches for a child with the name given. + * + * @param name the name of the child + * + * @return an Optional containing the child found or empty + */ + Optional> findChildNamed(String name); + + /** + * Returns the child with the given name. If one can't be found a + * NodeException is thrown. + * + * @param name the name of the child + * + * @return the node + */ + Node getChildNamed(String name); + + /** + * Draw a representation of the tree. + * + * @param depth current depth for recursion + * + * @return a representation of the tree + */ + String drawTree(int depth); + + /** + * Returns true if the Node has a name. + * + * @return true if the node has a name + */ + boolean isNamed(); + + /** + * Remove the node from the children. + * + * @param node the node to be removed + */ + void removeChild(Node node); + + /** + * Removes the parent from the node. Makes the node into a new root node. + */ + void removeParent(); } diff --git a/src/main/java/net/kemitix/node/NodeItem.java b/src/main/java/net/kemitix/node/NodeItem.java index d11ff23..1ef3bf6 100644 --- a/src/main/java/net/kemitix/node/NodeItem.java +++ b/src/main/java/net/kemitix/node/NodeItem.java @@ -1,9 +1,11 @@ package net.kemitix.node; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; /** * Represents a tree of nodes. @@ -16,33 +18,108 @@ public class NodeItem implements Node { private final T data; - private Node parent; - private final Set> children = new HashSet<>(); + private Function, String> nameSupplier; + + private Node parent; + + private String name; + /** - * Creates a root node. + * Create named root node. * - * @param data the value of the node + * @param data the data or null + * @param name the name + */ + public NodeItem(final T data, final String name) { + this(data); + this.name = name; + } + + /** + * Create unnamed root node. + * + * @param data the data or null */ public NodeItem(final T data) { - this(data, null); + this.data = data; + this.nameSupplier = (n) -> null; + } + + /** + * Creates root node with a name supplier. + * + * @param data the data or null + * @param nameSupplier the name supplier function + */ + public NodeItem( + final T data, final Function, String> nameSupplier) { + this(data); + this.nameSupplier = nameSupplier; + name = generateName(); } /** * Creates a node with a parent. * - * @param data the value of the node + * @param data the data or null * @param parent the parent node */ public NodeItem(final T data, final Node parent) { - if (data == null) { - throw new NullPointerException("data"); - } this.data = data; - if (parent != null) { - setParent(parent); + setParent(parent); + this.name = generateName(); + } + + /** + * Creates a named node with a parent. + * + * @param data the data or null + * @param name the name + * @param parent the parent node + */ + public NodeItem(final T data, final String name, final Node parent) { + this.data = data; + this.name = name; + setParent(parent); + } + + /** + * Creates a node with a name supplier and a parent. + * + * @param data the data or null + * @param nameSupplier the name supplier function + * @param parent the parent node + */ + public NodeItem( + final T data, final Function, String> nameSupplier, + final Node parent) { + this(data, nameSupplier); + setParent(parent); + } + + private String generateName() { + return getNameSupplier().apply(this); + } + + private Function, String> getNameSupplier() { + if (nameSupplier != null) { + return nameSupplier; } + // no test for parent as root nodes will always have a default name + // supplier + return ((NodeItem) parent).getNameSupplier(); + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(final String name) { + this.name = name; } @Override @@ -50,6 +127,11 @@ public class NodeItem implements Node { return data; } + @Override + public boolean isEmpty() { + return data == null; + } + @Override public Node getParent() { return parent; @@ -60,26 +142,6 @@ public class NodeItem implements Node { return children; } - /** - * Make the current node a direct child of the parent. - * - * @param parent the new parent node - */ - @Override - public final void setParent(final Node parent) { - if (parent == null) { - throw new NullPointerException("parent"); - } - if (this.equals(parent) || parent.isChildOf(this)) { - throw new NodeException("Parent is a descendant"); - } - if (this.parent != null) { - this.parent.getChildren().remove(this); - } - this.parent = parent; - parent.addChild(this); - } - /** * Adds the child to the node. * @@ -93,6 +155,14 @@ public class NodeItem implements Node { if (this.equals(child) || isChildOf(child)) { throw new NodeException("Child is an ancestor"); } + if (child.isNamed()) { + final Optional> existingChild = findChildNamed( + child.getName()); + if (existingChild.isPresent() && existingChild.get() != child) { + throw new NodeException( + "Node with that name already exists here"); + } + } children.add(child); if (child.getParent() == null || !child.getParent().equals(this)) { child.setParent(this); @@ -100,40 +170,18 @@ public class NodeItem implements Node { } /** - * Checks if the node is an ancestor. + * Creates a new node and adds it as a child of the current node. * - * @param node the potential ancestor + * @param child the child node's data * - * @return true if the node is an ancestor + * @return the new child node */ @Override - public boolean isChildOf(final Node node) { - return parent != null && (node.equals(parent) || parent.isChildOf( - node)); - } - - /** - * Walks the node tree using the path to select each child. - * - * @param path the path to the desired child - * - * @return the child or null - */ - @Override - public Optional> walkTree(final List path) { - if (path == null) { - throw new NullPointerException("path"); + public Node createChild(final T child) { + if (child == null) { + throw new NullPointerException("child"); } - if (path.size() > 0) { - Optional> found = getChild(path.get(0)); - if (found.isPresent()) { - if (path.size() > 1) { - return found.get().walkTree(path.subList(1, path.size())); - } - return found; - } - } - return Optional.empty(); + return new NodeItem<>(child, this); } /** @@ -187,18 +235,158 @@ public class NodeItem implements Node { } /** - * Creates a new node and adds it as a child of the current node. + * Checks if the node is an ancestor. * - * @param child the child node's data + * @param node the potential ancestor * - * @return the new child node + * @return true if the node is an ancestor */ @Override - public Node createChild(final T child) { - if (child == null) { - throw new NullPointerException("child"); + public boolean isChildOf(final Node node) { + return parent != null && (node.equals(parent) || parent.isChildOf( + node)); + } + + /** + * Make the current node a direct child of the parent. + * + * @param parent the new parent node + */ + @Override + public final void setParent(final Node parent) { + if (parent == null) { + throw new NullPointerException("parent"); + } + if (this.equals(parent) || parent.isChildOf(this)) { + throw new NodeException("Parent is a descendant"); + } + if (this.parent != null) { + this.parent.getChildren().remove(this); + } + this.parent = parent; + parent.addChild(this); + } + + /** + * Walks the node tree using the path to select each child. + * + * @param path the path to the desired child + * + * @return the child or null + */ + @Override + public Optional> walkTree(final List path) { + if (path == null) { + throw new NullPointerException("path"); + } + if (path.size() > 0) { + Optional> found = getChild(path.get(0)); + if (found.isPresent()) { + if (path.size() > 1) { + return found.get().walkTree(path.subList(1, path.size())); + } + return found; + } + } + return Optional.empty(); + } + + @Override + public void placeNodeIn(final Node nodeItem, final String... path) { + if (path.length == 0) { + if (!nodeItem.isNamed()) { // nothing to conflict with + addChild(nodeItem); + return; + } + final Optional> childNamed = findChildNamed( + nodeItem.getName()); + if (!childNamed.isPresent()) { // nothing with the same name exists + addChild(nodeItem); + return; + } + // we have an existing node with the same name + final Node existing = childNamed.get(); + if (!existing.isEmpty()) { + throw new NodeException( + "A non-empty node with that name already exists here"); + } else { + existing.getChildren().forEach(nodeItem::addChild); + existing.removeParent(); + addChild(nodeItem); + } + return; + } + String item = path[0]; + final Optional> childNamed = findChildNamed(item); + Node child; + if (!childNamed.isPresent()) { + child = new NodeItem<>(null, item, this); + } else { + child = childNamed.get(); + } + child.placeNodeIn(nodeItem, Arrays.copyOfRange(path, 1, path.length)); + } + + @Override + public Optional> findChildNamed(final String named) { + if (named == null) { + throw new NullPointerException("name"); + } + return children.stream() + .filter((Node t) -> t.getName().equals(named)) + .findAny(); + } + + @Override + public Node getChildNamed(final String named) { + final Optional> optional = findChildNamed(named); + if (optional.isPresent()) { + return optional.get(); + } + throw new NodeException("Named child not found"); + } + + @Override + public String drawTree(final int depth) { + final StringBuilder sb = new StringBuilder(); + final String unnamed = "(unnamed)"; + if (isNamed()) { + sb.append(String.format("[%1$" + (depth + name.length()) + "s]\n", + name)); + } else if (!children.isEmpty()) { + sb.append( + String.format("[%1$" + (depth + unnamed.length()) + "s]\n", + unnamed)); + } + getChildren().stream().forEach(c -> sb.append(c.drawTree(depth + 1))); + return sb.toString(); + } + + @Override + public boolean isNamed() { + return name != null && name.length() > 0; + } + + @Override + public void removeChild(final Node node) { + if (children.remove(node)) { + node.removeParent(); + } + } + + @Override + public void removeParent() { + if (parent != null) { + Node oldParent = parent; + Function, String> supplier = getNameSupplier(); + parent = null; + oldParent.removeChild(this); + if (this.nameSupplier == null) { + // this is now a root node, so must provide a default name + // supplier + this.nameSupplier = supplier; + } } - return new NodeItem<>(child, this); } } diff --git a/src/test/java/net/kemitix/node/NodeItemTest.java b/src/test/java/net/kemitix/node/NodeItemTest.java index e22605f..200321f 100644 --- a/src/test/java/net/kemitix/node/NodeItemTest.java +++ b/src/test/java/net/kemitix/node/NodeItemTest.java @@ -1,13 +1,15 @@ package net.kemitix.node; import lombok.val; -import org.junit.Assert; +import org.assertj.core.api.SoftAssertions; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; +import static org.assertj.core.api.Assertions.assertThat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collections; import java.util.Optional; @@ -19,32 +21,51 @@ import java.util.Optional; */ public class NodeItemTest { - /** - * Class under test. - */ + @Rule + public ExpectedException exception = ExpectedException.none(); + private Node node; - /** - * Test that node data is recoverable. - */ @Test - public void shouldReturnNodeData() { + public void getDataReturnsData() { //given val data = "this node data"; //when node = new NodeItem<>(data); //then - Assert.assertThat("can get the data from a node", node.getData(), - is(data)); + assertThat(node.getData()).as("can get the data from a node"). + isSameAs(data); } - /** - * Test that passing null as node data throws exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenDataIsNull() { + @Test + public void canCreateAnEmptyAndUnnamedNode() { //when node = new NodeItem<>(null); + //then + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(node.isEmpty()).as("node is empty").isTrue(); + softly.assertThat(node.isNamed()).as("node is unnamed").isFalse(); + softly.assertAll(); + } + + @Test + public void canCreateNodeWithParentAndCustomNameSupplier() { + //given + node = new NodeItem<>(null, n -> "root name supplier"); + //when + val child = new NodeItem<>(null, n -> "overridden", node); + //then + assertThat(child.getName()).isEqualTo("overridden"); + } + + @Test + public void canSetName() { + //given + node = new NodeItem<>(null); + //when + node.setName("named"); + //then + assertThat(node.getName()).isEqualTo("named"); } /** @@ -53,10 +74,10 @@ public class NodeItemTest { @Test public void shouldHaveNullForDefaultParent() { //given - node = new NodeItem<>("data"); + node = new NodeItem<>("data", Node::getData); //then - Assert.assertThat("node created without a parent has null as parent", - node.getParent(), nullValue()); + assertThat(node.getParent()).as( + "node created without a parent has null as parent").isNull(); } /** @@ -65,23 +86,26 @@ public class NodeItemTest { @Test public void shouldReturnNodeParent() { //given - val parent = new NodeItem("parent"); + val parent = new NodeItem("parent", Node::getData); //when node = new NodeItem<>("subject", parent); //then - Assert.assertThat("node created with a parent can return the parent", - node.getParent(), is(parent)); + assertThat(node.getParent()).as( + "node created with a parent can return the parent") + .isSameAs(parent); } /** * Test that setting the parent on a node where the proposed parent is a * child of the node throws an exception. */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenSettingParentToAChild() { + @Test + public void setParentShouldThrowNodeExceptionWhenParentIsAChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val child = new NodeItem("child", node); + exception.expect(NodeException.class); + exception.expectMessage("Parent is a descendant"); //when node.setParent(child); } @@ -91,16 +115,16 @@ public class NodeItemTest { * child of the parent. */ @Test + @SuppressWarnings("unchecked") public void shouldAddNewNodeAsChildToParent() { //given - val parent = new NodeItem("parent"); + val parent = new NodeItem("parent", Node::getData); //when node = new NodeItem<>("subject", parent); //then - Assert.assertThat( + assertThat(parent.getChildren()).as( "when a node is created with a parent, the parent has the new" - + " node among it's children", parent.getChildren(), - hasItem(node)); + + " node among it's children").contains(node); } /** @@ -109,23 +133,25 @@ public class NodeItemTest { @Test public void shouldReturnSetParent() { //given - node = new NodeItem<>("subject"); - val parent = new NodeItem("parent"); + node = new NodeItem<>("subject", Node::getData); + val parent = new NodeItem("parent", Node::getData); //when node.setParent(parent); //then - Assert.assertThat( + assertThat(node.getParent()).as( "when a node is assigned a new parent that parent can be " - + "returned", node.getParent(), is(parent)); + + "returned").isSameAs(parent); } /** * Test that we throw an exception when passed null. */ - @Test(expected = NullPointerException.class) + @Test public void shouldThrowNPEWhenSetParentNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("parent"); //when node.setParent(null); } @@ -134,10 +160,12 @@ public class NodeItemTest { * Test that we throw an exceptions when attempting to node as its own * parent. */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenSetParentSelf() { + @Test + public void setParentShouldThrowNodeExceptionWhenParentIsSelf() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Parent is a descendant"); //when node.setParent(node); } @@ -149,19 +177,18 @@ public class NodeItemTest { @Test public void shouldUpdateOldParentWhenNodeSetToNewParent() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val child = node.createChild("child"); - val newParent = new NodeItem("newParent"); + val newParent = new NodeItem("newParent", Node::getData); //when child.setParent(newParent); //then - Assert.assertThat( + assertThat(child.getParent()).as( "when a node is assigned a new parent, the old parent is " - + "replaced", child.getParent(), is(newParent)); - Assert.assertThat( + + "replaced").isSameAs(newParent); + assertThat(node.getChild("child").isPresent()).as( "when a node is assigned a new parent, the old parent no " - + "longer has the node among it's children", - node.getChild("child").isPresent(), is(false)); + + "longer has the node among it's children").isFalse(); } /** @@ -171,30 +198,31 @@ public class NodeItemTest { @Test public void shouldRemoveNodeFromOldParentWhenAddedAsChildToNewParent() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val child = node.createChild("child"); - val newParent = new NodeItem("newParent"); + val newParent = new NodeItem("newParent", Node::getData); //when newParent.addChild(child); //then - Assert.assertThat( + assertThat(child.getParent()).as( "when a node with an existing parent is added as a child " - + "to another node, then the old parent is replaced", - child.getParent(), is(newParent)); - Assert.assertThat( + + "to another node, then the old parent is replaced") + .isSameAs(newParent); + assertThat(node.getChild("child").isPresent()).as( "when a node with an existing parent is added as a child to " + "another node, then the old parent no longer has " - + "the node among it's children", - node.getChild("child").isPresent(), is(false)); + + "the node among it's children").isFalse(); } /** * Test that adding null as a child throws an exception. */ - @Test(expected = NullPointerException.class) + @Test public void shouldThrowNPEWhenAddingNullAsChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); //when node.addChild(null); } @@ -203,25 +231,28 @@ public class NodeItemTest { * Test that adding a child is returned. */ @Test + @SuppressWarnings("unchecked") public void shouldReturnAddedChild() { //given - node = new NodeItem<>("subject"); - val child = new NodeItem("child"); + node = new NodeItem<>("subject", Node::getData); + val child = new NodeItem("child", Node::getData); //when node.addChild(child); //then - Assert.assertThat( + assertThat(node.getChildren()).as( "when a node is added as a child, the node is among the " - + "children", node.getChildren(), hasItem(child)); + + "children").contains(child); } /** * Test that adding a node as it's own child throws an exception. */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenAddingANodeAsOwnChild() { + @Test + public void addChildShouldThrowNodeExceptionWhenAddingANodeAsOwnChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); //then node.addChild(node); } @@ -229,10 +260,12 @@ public class NodeItemTest { /** * Test that adding a node to itself as a child causes an exception. */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingSelfAsChild() { + @Test + public void addChildShouldThrowNodeExceptionWhenAddingSelfAsChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); //when node.addChild(node); } @@ -241,11 +274,13 @@ public class NodeItemTest { * Test that adding the parent of a node to the node as a child causes an * exception. */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingParentAsChild() { + @Test + public void addChildShouldThrowNodeExceptionWhenChildIsParent() { //given - val parent = new NodeItem("parent"); + val parent = new NodeItem("parent", Node::getData); node = new NodeItem<>("subject", parent); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); //when node.addChild(parent); } @@ -254,12 +289,14 @@ public class NodeItemTest { * Test that adding the grandparent to a node as a child causes an * exception. */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingGrandParentAsChild() { + @Test + public void addChildShouldThrowNodeExceptionWhenAddingGrandParentAsChild() { //given - val grandParent = new NodeItem("grandparent"); + val grandParent = new NodeItem("grandparent", Node::getData); val parent = new NodeItem("parent", grandParent); node = new NodeItem<>("subject", parent); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); //when node.addChild(grandParent); } @@ -270,14 +307,14 @@ public class NodeItemTest { @Test public void shouldSetParentOnChildWhenAddedAsChild() { //given - val child = new NodeItem("child"); - node = new NodeItem<>("subject"); + val child = new NodeItem("child", Node::getData); + node = new NodeItem<>("subject", Node::getData); //when node.addChild(child); //then - Assert.assertThat( + assertThat(child.getParent()).as( "when a node is added as a child, the child has the node as " - + "its parent", child.getParent(), is(node)); + + "its parent").isSameAs(node); } /** @@ -287,7 +324,7 @@ public class NodeItemTest { public void shouldWalkTreeToNode() { //given val grandparent = "grandparent"; - val grandParentNode = new NodeItem(grandparent); + val grandParentNode = new NodeItem(grandparent, Node::getData); val parent = "parent"; val parentNode = new NodeItem(parent, grandParentNode); val subject = "subject"; @@ -295,12 +332,12 @@ public class NodeItemTest { //when val result = grandParentNode.walkTree(Arrays.asList(parent, subject)); //then - Assert.assertThat("when we walk the tree to a node it is found", - result.isPresent(), is(true)); + assertThat(result.isPresent()).as( + "when we walk the tree to a node it is found").isTrue(); if (result.isPresent()) { - Assert.assertThat( - "when we walk the tree to a node the correct node is found", - result.get(), is(node)); + assertThat(result.get()).as( + "when we walk the tree to a node the correct node is found") + .isSameAs(node); } } @@ -312,24 +349,26 @@ public class NodeItemTest { public void shouldNotFindNonExistentChildNode() { //given val parent = "parent"; - val parentNode = new NodeItem(parent); + val parentNode = new NodeItem(parent, Node::getData); val subject = "subject"; node = new NodeItem<>(subject, parentNode); //when val result = parentNode.walkTree(Arrays.asList(subject, "no child")); //then - Assert.assertThat( + assertThat(result.isPresent()).as( "when we walk the tree to a node that doesn't exists, nothing" - + " is found", result.isPresent(), is(false)); + + " is found").isFalse(); } /** * Test that when we pass null we get an exception. */ - @Test(expected = NullPointerException.class) + @Test public void shouldThrowNEWhenWalkTreeNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("path"); //when node.walkTree(null); } @@ -341,9 +380,11 @@ public class NodeItemTest { @Test public void shouldReturnEmptyForEmptyWalkTreePath() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); //when - node.walkTree(Collections.emptyList()); + val result = node.walkTree(Collections.emptyList()); + //then + assertThat(result).isEmpty(); } /** @@ -352,7 +393,7 @@ public class NodeItemTest { @Test public void shouldCreateDescendantNodes() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val alphaData = "alpha"; val betaData = "beta"; val gammaData = "gamma"; @@ -361,36 +402,34 @@ public class NodeItemTest { Arrays.asList(alphaData, betaData, gammaData)); //then val alphaOptional = node.getChild(alphaData); - Assert.assertThat( - "when creating a descendant line, the first element is found", - alphaOptional.isPresent(), is(true)); + assertThat(alphaOptional.isPresent()).as( + "when creating a descendant line, the first element is found") + .isTrue(); if (alphaOptional.isPresent()) { val alpha = alphaOptional.get(); - Assert.assertThat( + assertThat(alpha.getParent()).as( "when creating a descendant line, the first element has " - + "the current node as its parent", - alpha.getParent(), is(node)); + + "the current node as its parent").isSameAs(node); val betaOptional = alpha.getChild(betaData); - Assert.assertThat( + assertThat(betaOptional.isPresent()).as( "when creating a descendant line, the second element is " - + "found", betaOptional.isPresent(), is(true)); + + "found").isTrue(); if (betaOptional.isPresent()) { val beta = betaOptional.get(); - Assert.assertThat( + assertThat(beta.getParent()).as( "when creating a descendant line, the second element " - + "has the first as its parent", - beta.getParent(), is(alpha)); + + "has the first as its parent") + .isSameAs(alpha); val gammaOptional = beta.getChild(gammaData); - Assert.assertThat( + assertThat(gammaOptional.isPresent()).as( "when creating a descendant line, the third element " - + "is found", gammaOptional.isPresent(), - is(true)); + + "is found").isTrue(); if (gammaOptional.isPresent()) { val gamma = gammaOptional.get(); - Assert.assertThat( + assertThat(gamma.getParent()).as( "when creating a descendant line, the third " - + "element has the second as its parent", - gamma.getParent(), is(beta)); + + "element has the second as its parent") + .isSameAs(beta); } } } @@ -400,10 +439,12 @@ public class NodeItemTest { * Test that if we pass null to create a chain of descendant nodes we get an * exception. */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenCreateDescendantNull() { + @Test + public void createDescendantLineShouldThrowNPEWhenDescendantsAreNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("descendants"); //when node.createDescendantLine(null); } @@ -414,13 +455,13 @@ public class NodeItemTest { @Test public void shouldChangeNothingWhenCreateDescendantEmpty() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); //when node.createDescendantLine(Collections.emptyList()); //then - Assert.assertThat( + assertThat(node.getChildren()).as( "when creating a descendant line from an empty list, nothing " - + "is created", node.getChildren().size(), is(0)); + + "is created").isEmpty(); } /** @@ -429,15 +470,15 @@ public class NodeItemTest { @Test public void shouldFindExistingChildNode() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val childData = "child"; val child = new NodeItem(childData, node); //when val found = node.findOrCreateChild(childData); //then - Assert.assertThat( + assertThat(found).as( "when searching for a child by data, the matching child is " - + "found", found, is(child)); + + "found").isSameAs(child); } /** @@ -446,23 +487,25 @@ public class NodeItemTest { @Test public void shouldFindCreateNewChildNode() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val childData = "child"; //when val found = node.findOrCreateChild(childData); //then - Assert.assertThat( - "when searching for a child by data, a new node is created", - found.getData(), is(childData)); + assertThat(found.getData()).as( + "when searching for a child by data, a new node is created") + .isSameAs(childData); } /** * Test that if we pass null we get an exception. */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEFWhenFindOrCreateChildNull() { + @Test + public void findOrCreateChildShouldThrowNPEFWhenChildIsNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); //when node.findOrCreateChild(null); } @@ -473,29 +516,31 @@ public class NodeItemTest { @Test public void shouldGetChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val childData = "child"; - val child = new NodeItem(childData); + val child = new NodeItem(childData, Node::getData); node.addChild(child); //when val found = node.getChild(childData); //then - Assert.assertThat("when retrieving a child by its data, it is found", - found.isPresent(), is(true)); + assertThat(found.isPresent()).as( + "when retrieving a child by its data, it is found").isTrue(); if (found.isPresent()) { - Assert.assertThat( + assertThat(found.get()).as( "when retrieving a child by its data, it is the expected " - + "node", found.get(), is(child)); + + "node").isSameAs(child); } } /** * Test that we throw an exception when passed null. */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenGetChildNull() { + @Test + public void getChildShouldThrowNPEWhenThereIsNoChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("data", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); //when node.getChild(null); } @@ -507,34 +552,330 @@ public class NodeItemTest { @Test public void shouldCreateChild() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); val childData = "child"; //when val child = node.createChild(childData); //then - Assert.assertThat( + assertThat(child.getParent()).as( "when creating a child node, the child has the current node " - + "as its parent", child.getParent(), is(node)); + + "as its parent").isSameAs(node); val foundChild = node.getChild(childData); - Assert.assertThat( + assertThat(foundChild.isPresent()).as( "when creating a child node, the child can be found by its " - + "data", foundChild.isPresent(), is(true)); + + "data").isTrue(); if (foundChild.isPresent()) { - Assert.assertThat( + assertThat(foundChild.get()).as( "when creating a child node, the correct child can be " - + "found by its data", foundChild.get(), is(child)); + + "found by its data").isSameAs(child); } } /** * Test that we throw an exception when passed null. */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenCreateChildNull() { + @Test + public void createChildShouldThrowNPEWhenChildIsNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); //when node.createChild(null); } + @Test + public void getNameShouldBeCorrect() { + //given + node = new NodeItem<>("subject", Node::getData); + //then + assertThat(node.getName()).isEqualTo("subject"); + } + + @Test + public void getNameShouldUseParentNameSupplier() { + //given + val root = new NodeItem("root", Node::getData); + node = new NodeItem<>("child", root); + //then + assertThat(node.getName()).isEqualTo("child"); + } + + @Test + public void getNameShouldReturnNameForNonStringData() { + val root = new NodeItem(LocalDate.parse("2016-05-23"), + n -> n.getData().format(DateTimeFormatter.BASIC_ISO_DATE)); + //then + assertThat(root.getName()).isEqualTo("20160523"); + } + + @Test + public void getNameShouldUseClosestNameSupplier() { + node = new NodeItem<>("root", Node::getData); + val child = new NodeItem("child", Object::toString); + node.addChild(child); + val grandChild = new NodeItem<>("grandchild", child); + //then + assertThat(node.getName()).isEqualTo("root"); + assertThat(child.getName()).isNotEqualTo("child"); + assertThat(grandChild.getName()).isNotEqualTo("grandchild"); + } + + @Test + public void getNameShouldWorkWithoutNameSupplier() { + node = new NodeItem<>(null, "root"); + val namedchild = new NodeItem<>("named", "Alice", node); + //then + assertThat(node.getName()).isEqualTo("root"); + assertThat(namedchild.getName()).isEqualTo("Alice"); + } + + @Test + public void canCreateRootNodeWithoutData() { + node = new NodeItem<>(null, "empty"); + assertThat(node.getData()).isNull(); + } + + @Test + public void canCreateRootNodeWithoutDataButWithNameSupplier() { + node = new NodeItem<>(null, Node::getData); + assertThat(node.getData()).isNull(); + } + + @Test + public void getChildNamedFindsChild() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + val beta = new NodeItem(null, "beta"); + node.addChild(alpha); + node.addChild(beta); + //when + val result = node.getChildNamed("alpha"); + //then + assertThat(result).isSameAs(alpha); + } + + @Test + public void getChildNamedFindsNothing() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + val beta = new NodeItem(null, "beta"); + node.addChild(alpha); + node.addChild(beta); + exception.expect(NodeException.class); + exception.expectMessage("Named child not found"); + //when + node.getChildNamed("gamma"); + } + + @Test + public void nodeNamesAreUniqueWithinAParent() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + node.addChild(alpha); + val beta = new NodeItem(null, "alpha"); + exception.expect(NodeException.class); + exception.expectMessage("Node with that name already exists here"); + //when + node.addChild(beta); + } + + @Test + public void canPlaceNodeInTreeByPathNames() { + //given + node = new NodeItem<>(null, "root"); // create a root + val four = new NodeItem("data", "four"); + //when + node.placeNodeIn(four, "one", "two", "three"); + //then + val three = four.getParent(); + assertThat(four.getParent()).as("add node to a tree").isNotNull(); + assertThat(three.getName()).isEqualTo("three"); + val two = three.getParent(); + assertThat(two.getName()).isEqualTo("two"); + val one = two.getParent(); + assertThat(one.getName()).isEqualTo("one"); + assertThat(one.getParent()).isSameAs(node); + assertThat(node.getChildNamed("one") + .getChildNamed("two") + .getChildNamed("three") + .getChildNamed("four")).isSameAs(four); + } + + @Test + @SuppressWarnings("unchecked") + public void canPlaceInTreeUnderExistingNode() { + //given + node = new NodeItem<>(null, "root"); + val child = new NodeItem("child data", "child"); + val grandchild = new NodeItem("grandchild data", "grandchild"); + //when + node.placeNodeIn(child); // as root/child + node.placeNodeIn(grandchild, "child"); // as root/child/grandchild + //then + assertThat(node.getChildNamed("child")).as("child").isSameAs(child); + assertThat(node.getChildNamed("child").getChildNamed("grandchild")).as( + "grandchild").isSameAs(grandchild); + } + + @Test + @SuppressWarnings("unchecked") + public void canPlaceInTreeAboveExistingNode() { + //given + node = new NodeItem<>(null, "root"); + val child = new NodeItem("child data", "child"); + val grandchild = new NodeItem("grandchild data", "grandchild"); + //when + node.placeNodeIn(grandchild, "child"); + node.placeNodeIn(child); + //then + assertThat(node.getChildNamed("child")).as("child").isSameAs(child); + assertThat(node.getChildNamed("child").getChildNamed("grandchild")).as( + "grandchild").isSameAs(grandchild); + } + + @Test + public void removingParentFromNodeWithNoParentIsNoop() { + //given + node = new NodeItem<>(null); + //when + node.removeParent(); + } + + @Test + public void placeNodeInTreeWhereNonEmptyNodeWithSameNameExists() { + //given + exception.expect(NodeException.class); + exception.expectMessage( + "A non-empty node with that name already exists here"); + node = new NodeItem<>(null); + val child = new NodeItem(null, "child", node); + new NodeItem<>("data", "grandchild", child); + // root -> child -> grandchild + // only grandchild has data + //when + // attempt to add another node called 'grandchild' to 'child' + node.placeNodeIn(new NodeItem<>("cuckoo", "grandchild"), "child"); + } + + @Test + @SuppressWarnings("unchecked") + public void placeNodeInTreeWhenAddedNodeIsUnnamed() { + //given + node = new NodeItem<>(null); + final Node newNode = new NodeItem<>(null); + //when + node.placeNodeIn(newNode); + //then + assertThat(node.getChildren()).containsOnly(newNode); + } + + @Test + @SuppressWarnings("unchecked") + public void placeNodeInTreeWhenEmptyChildWithTargetNameExists() { + //given + node = new NodeItem<>(null); + final NodeItem child = new NodeItem<>(null, "child"); + final NodeItem target = new NodeItem<>(null, "target"); + node.addChild(child); + child.addChild(target); + final NodeItem addMe = new NodeItem<>("I'm new", "target"); + assertThat(addMe.getParent()).isNull(); + //when + // addMe should replace target as the sole descendant of child + node.placeNodeIn(addMe, "child"); + //then + assertThat(child.getChildren()).as("child only contains new node") + .containsOnly(addMe); + assertThat(target.getParent()).as("old node is removed from tree") + .isNull(); + } + + @Test + public void findChildNamedShouldThrowNPEWhenNameIsNull() { + //given + exception.expect(NullPointerException.class); + exception.expectMessage("name"); + node = new NodeItem<>(null); + //when + node.findChildNamed(null); + } + + @Test + public void isNamedNull() { + //given + node = new NodeItem<>(null); + //then + assertThat(node.isNamed()).isFalse(); + } + + @Test + public void isNamedEmpty() { + //given + node = new NodeItem<>(null, ""); + //then + assertThat(node.isNamed()).isFalse(); + } + + @Test + public void isNamedNamed() { + //given + node = new NodeItem<>(null, "named"); + //then + assertThat(node.isNamed()).isTrue(); + } + + @Test + public void removeParentNodeProvidesSameNameSupplier() { + // once a node has it's parent removed it should provide a default name + // provider + //given + node = new NodeItem<>("data", Node::getData); // name provider: getData + final NodeItem child = new NodeItem<>("other", node); + assertThat(node.getName()).as("initial root name").isEqualTo("data"); + assertThat(child.getName()).as("initial child name").isEqualTo("other"); + //when + child.removeParent(); + //then + assertThat(node.getName()).as("final root name").isEqualTo("data"); + assertThat(child.getName()).as("final child name").isEqualTo("other"); + } + + @Test + @SuppressWarnings("unchecked") + public void removeChildRemovesTheChild() { + //given + node = new NodeItem<>(null); + Node child = node.createChild("child"); + assertThat(node.getChildren()).containsExactly(child); + //then + node.removeChild(child); + //then + assertThat(node.getChildren()).isEmpty(); + } + + @Test + public void drawTreeIsCorrect() { + //given + node = new NodeItem<>(null, "root"); + val bob = new NodeItem(null, "bob", node); + val alice = new NodeItem(null, "alice", node); + new NodeItem<>(null, "dave", alice); + new NodeItem<>(null, bob); // has no name and no children so no included + val kim = new NodeItem(null, node); // nameless mother + new NodeItem<>(null, "lucy", kim); + //when + val tree = node.drawTree(0); + //then + String[] lines = tree.split("\n"); + assertThat(lines).contains("[root]", "[ alice]", "[ dave]", + "[ (unnamed)]", "[ lucy]", "[ bob]"); + assertThat(lines).containsSubsequence("[root]", "[ alice]", "[ dave]"); + assertThat(lines).containsSubsequence("[root]", "[ (unnamed)]", + "[ lucy]"); + assertThat(lines).containsSubsequence("[root]", "[ bob]"); + } }