From 8cd57711b84ea7dcf5150c9dbd853165b7cc6474 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 9 Jan 2016 17:08:52 +0000 Subject: [PATCH] Initial commit --- .gitignore | 2 + checkstyle.xml | 192 +++++++ pom.xml | 53 ++ src/main/java/net/kemitix/node/Node.java | 108 ++++ .../java/net/kemitix/node/NodeException.java | 20 + src/main/java/net/kemitix/node/NodeItem.java | 186 +++++++ .../java/net/kemitix/node/package-info.java | 4 + .../net/kemitix/node/NodeExceptionTest.java | 35 ++ .../java/net/kemitix/node/NodeItemTest.java | 496 ++++++++++++++++++ 9 files changed, 1096 insertions(+) create mode 100644 .gitignore create mode 100644 checkstyle.xml create mode 100644 pom.xml create mode 100644 src/main/java/net/kemitix/node/Node.java create mode 100644 src/main/java/net/kemitix/node/NodeException.java create mode 100644 src/main/java/net/kemitix/node/NodeItem.java create mode 100644 src/main/java/net/kemitix/node/package-info.java create mode 100644 src/test/java/net/kemitix/node/NodeExceptionTest.java create mode 100644 src/test/java/net/kemitix/node/NodeItemTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c89b474 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/nbproject diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..e54bdb6 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9675f0f --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + net.kemitix + node + 0.1.0-SNAPSHOT + jar + + Node + A parent/children data structure + + + net.kemitix + kemitix-parent + 0.6.0 + + + + https://github.com/kemitix/node/issues + GitHub Issues + + + + scm:git:git@github.com:kemitix/node.git + scm:git:git@github.com:kemitix/node.git + git@github.com:kemitix/node.git + + + https://github.com/kemitix/node + + 2016 + + + + org.projectlombok + lombok + 1.16.6 + + + + junit + junit + 4.12 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + \ No newline at end of file diff --git a/src/main/java/net/kemitix/node/Node.java b/src/main/java/net/kemitix/node/Node.java new file mode 100644 index 0000000..2dae709 --- /dev/null +++ b/src/main/java/net/kemitix/node/Node.java @@ -0,0 +1,108 @@ +package net.kemitix.node; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * An interface for tree node items. + * + * @author pcampbell + * @param the type of data held in each node + */ +public interface Node { + + /** + * Fetch the data held within the node. + * + * @return the node's data + */ + T getData(); + + /** + * Fetch the parent node. + * + *

+ * If the node is a root node, i.e. has no parent, then this will return + * null. + * + * @return the parent node + */ + Node getParent(); + + /** + * Fetches the child nodes. + * + * @return the set of child nodes + */ + Set> getChildren(); + + /** + * Adds the child to the node. + * + * @param child the node to add + */ + void addChild(final Node child); + + /** + * Creates a new node and adds it as a child of the current node. + * + * @param child the child node's data + * + * @return the new child node + */ + Node createChild(final T child); + + /** + * Populates the tree with the path of nodes, each being a child of the + * previous node in the path. + * + * @param descendants the line of descendants from the current node + */ + void createDescendantLine(final List descendants); + + /** + * Looks for a child node and returns it, creating a new child node if one + * isn't found. + * + * @param child the child's data to search or create with + * + * @return the found or created child node + */ + Node findOrCreateChild(final T child); + + /** + * Fetches the node for the child if present. + * + * @param child the child's data to search for + * + * @return an {@link Optional} containing the child node if found + */ + Optional> getChild(final T child); + + /** + * Checks if the node is an ancestor. + * + * @param node the potential ancestor + * + * @return true if the node is an ancestor + */ + 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. + * + * @param path the path to the desired child + * + * @return the child or null + */ + Optional> walkTree(final List path); + +} diff --git a/src/main/java/net/kemitix/node/NodeException.java b/src/main/java/net/kemitix/node/NodeException.java new file mode 100644 index 0000000..7f84643 --- /dev/null +++ b/src/main/java/net/kemitix/node/NodeException.java @@ -0,0 +1,20 @@ +package net.kemitix.node; + +/** + * Represents an error within the tree node. + * + * @author pcampbell + */ +@SuppressWarnings("serial") +public class NodeException extends RuntimeException { + + /** + * Constructor with message. + * + * @param message the message + */ + public NodeException(final String message) { + super(message); + } + +} diff --git a/src/main/java/net/kemitix/node/NodeItem.java b/src/main/java/net/kemitix/node/NodeItem.java new file mode 100644 index 0000000..d35366a --- /dev/null +++ b/src/main/java/net/kemitix/node/NodeItem.java @@ -0,0 +1,186 @@ +package net.kemitix.node; + +import lombok.Getter; +import lombok.NonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Represents a tree of nodes. + * + * @param the type of data stored in each node + * + * @author pcampbell + */ +public class NodeItem implements Node { + + @Getter + private final T data; + + @Getter + private Node parent; + + @Getter + private Set> children; + + /** + * Creates a root node. + * + * @param data the value of the node + */ + public NodeItem(@NonNull final T data) { + this(data, null); + } + + /** + * Creates a node with a parent. + * + * @param data the value of the node + * @param parent the parent node + */ + public NodeItem(final T data, final Node parent) { + this.data = data; + if (parent != null) { + setParent(parent); + } + this.children = new HashSet<>(); + } + + /** + * Make the current node a direct child of the parent. + * + * @param parent the new parent node + */ + @Override + public final void setParent(@NonNull final Node 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. + * + * @param child the node to add + */ + @Override + public void addChild(@NonNull final Node child) { + if (this.equals(child) || isChildOf(child)) { + throw new NodeException("Child is an ancestor"); + } + children.add(child); + if (child.getParent() == null || !child.getParent().equals(this)) { + child.setParent(this); + } + } + + /** + * Checks if the node is an ancestor. + * + * @param node the potential ancestor + * + * @return true if the node is an ancestor + */ + @Override + public boolean isChildOf(final Node node) { + if (node.equals(parent)) { + return true; + } + if (parent != null) { + return parent.isChildOf(node); + } + return false; + } + + /** + * 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(@NonNull final List path) { + if (path.size() > 0) { + Optional> found = children.stream() + .filter((Node child) -> path.get(0) + .equals(child.getData())) + .findFirst(); + if (found.isPresent()) { + if (path.size() > 1) { + return found.get().walkTree(path.subList(1, path.size())); + } + return found; + } + } + return Optional.empty(); + } + + /** + * Populates the tree with the path of nodes, each being a child of the + * previous node in the path. + * + * @param descendants the line of descendants from the current node + */ + @Override + public void createDescendantLine(@NonNull final List descendants) { + if (!descendants.isEmpty()) { + findOrCreateChild(descendants.get(0)) + .createDescendantLine( + descendants.subList(1, descendants.size())); + } + } + + /** + * Looks for a child node and returns it, creating a new child node if one + * isn't found. + * + * @param child the child's data to search or create with + * + * @return the found or created child node + */ + @Override + public Node findOrCreateChild(@NonNull final T child) { + Optional> found = getChild(child); + if (found.isPresent()) { + return found.get(); + } else { + return createChild(child); + } + } + + /** + * Fetches the node for the child if present. + * + * @param child the child's data to search for + * + * @return an {@link Optional} containing the child node if found + */ + @Override + public Optional> getChild(@NonNull final T child) { + return children.stream() + .filter((Node t) -> t.getData().equals(child)) + .findAny(); + } + + /** + * Creates a new node and adds it as a child of the current node. + * + * @param child the child node's data + * + * @return the new child node + */ + @Override + public Node createChild(@NonNull final T child) { + return new NodeItem<>(child, this); + } + +} diff --git a/src/main/java/net/kemitix/node/package-info.java b/src/main/java/net/kemitix/node/package-info.java new file mode 100644 index 0000000..382684c --- /dev/null +++ b/src/main/java/net/kemitix/node/package-info.java @@ -0,0 +1,4 @@ +/** + * Tree Node implementation. + */ +package net.kemitix.node; diff --git a/src/test/java/net/kemitix/node/NodeExceptionTest.java b/src/test/java/net/kemitix/node/NodeExceptionTest.java new file mode 100644 index 0000000..5e9f7ac --- /dev/null +++ b/src/test/java/net/kemitix/node/NodeExceptionTest.java @@ -0,0 +1,35 @@ +package net.kemitix.node; + +import net.kemitix.node.NodeException; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link NodeException}. + * + * @author pcampbell + */ +public class NodeExceptionTest { + + /** + * Class under test. + */ + private NodeException nodeException; + + /** + * Test that message provided to constructor is returned. + */ + @Test + public void shouldReturnConstructorMessage() { + //given + final String message = "this is the message"; + //when + nodeException = new NodeException(message); + //then + assertThat(nodeException.getMessage(), is(message)); + } + +} diff --git a/src/test/java/net/kemitix/node/NodeItemTest.java b/src/test/java/net/kemitix/node/NodeItemTest.java new file mode 100644 index 0000000..c271ac3 --- /dev/null +++ b/src/test/java/net/kemitix/node/NodeItemTest.java @@ -0,0 +1,496 @@ +package net.kemitix.node; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Test for {@link NodeItem}. + * + * @author pcampbell + */ +public class NodeItemTest { + + /** + * Class under test. + */ + private Node node; + + /** + * Test {@link NodeItem#Node(java.lang.Object) } that node data is + * recoverable. + */ + @Test + public void shouldReturnNodeData() { + //given + final String data = "this node data"; + //when + node = new NodeItem<>(data); + //then + assertThat(node.getData(), is(data)); + } + + /** + * Test {@link NodeItem#Node(java.lang.Object) } that passing null as node + * data throws exception. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenDataIsNull() { + //when + node = new NodeItem<>(null); + } + + /** + * Test {@link NodeItem#Node(java.lang.Object) } that default node parent is + * null. + */ + @Test + public void shouldHaveNullForDefaulParent() { + //given + node = new NodeItem<>("data"); + //then + assertNull(node.getParent()); + } + + /** + * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } that + * provided node parent is returned. + */ + @Test + public void shouldReturnNodeParent() { + //given + Node parent = new NodeItem<>("parent"); + //when + node = new NodeItem<>("subject", parent); + //then + assertThat(node.getParent(), is(parent)); + } + + /** + * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } 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() { + //given + node = new NodeItem<>("subject"); + Node child = new NodeItem<>("child", node); + //when + node.setParent(child); + } + + /** + * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } that + * when parent is added to created node, the created node is now a child of + * the parent. + */ + @Test + public void shouldAddNewNodeAsChildToParent() { + //given + Node parent = new NodeItem<>("parent"); + //when + node = new NodeItem<>("subject", parent); + //then + assertThat(parent.getChildren(), hasItem(node)); + } + + /** + * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we return + * the same parent when set. + */ + @Test + public void shouldReturnSetParent() { + //given + node = new NodeItem<>("subject"); + Node parent = new NodeItem<>("parent"); + //when + node.setParent(parent); + //then + assertThat(node.getParent(), is(parent)); + } + + /** + * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we throw an + * exception when passed null. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenSetParentNull() { + //given + node = new NodeItem<>("subject"); + //when + node.setParent(null); + } + + /** + * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we throw an + * exceptions when attempting to node as its own parent. + */ + @Test(expected = NodeException.class) + public void shouldThrowNEWhenSetParentSelf() { + //given + node = new NodeItem<>("subject"); + //when + node.setParent(node); + } + + /** + * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that when a node + * with an existing parent is assigned a new parent, that the old parent no + * longer sees it as one of its children. + */ + @Test + public void shouldUpdateOldParentWhenNodeSetToNewParent() { + //given + node = new NodeItem<>("subject"); + Node child = node.createChild("child"); + Node newParent = new NodeItem<>("newParent"); + //when + child.setParent(newParent); + //then + assertThat(child.getParent(), is(newParent)); + assertFalse(node.getChild("child").isPresent()); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that when a node + * is added as a child to another node, that it's previous parent no longer + * has it as a child. + */ + @Test + public void shouldRemoveNodeFromOldParentWhenAddedAsChildToNewParent() { + //given + node = new NodeItem<>("subject"); + Node child = node.createChild("child"); + Node newParent = new NodeItem<>("newParent"); + //when + newParent.addChild(child); + //then + assertThat(child.getParent(), is(newParent)); + assertFalse(node.getChild("child").isPresent()); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding null + * as a child throws an exception. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenAddingNullAsChild() { + //given + node = new NodeItem<>("subject"); + //when + node.addChild(null); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a + * child is returned. + */ + @Test + public void shouldReturnAddedChild() { + //given + Node child = new NodeItem<>("child"); + node = new NodeItem<>("subject"); + //when + node.addChild(child); + //then + assertThat(node.getChildren(), hasItem(child)); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a node + * as it's own child throws an exception. + */ + @Test(expected = NodeException.class) + public void shouldThrowNEWhenAddingANodeAsOwnChild() { + //given + node = new NodeItem<>("subject"); + //then + node.addChild(node); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a node + * to itself as a child causes an exception. + */ + @Test(expected = NodeException.class) + public void shouldThrowWhenAddingSelfAsChild() { + //given + node = new NodeItem<>("subject"); + //when + node.addChild(node); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding the + * parent to node causes an exception. + */ + @Test(expected = NodeException.class) + public void shouldThrowWhenAddingParentAsChild() { + //given + Node parent = new NodeItem<>("parent"); + node = new NodeItem<>("subject", parent); + //when + node.addChild(parent); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding the + * grandparent to node causes an exception. + */ + @Test(expected = NodeException.class) + public void shouldThrowWhenAddingGrandParentAsChild() { + //given + Node grandParent = new NodeItem<>("grandparent"); + Node parent = new NodeItem<>("parent", grandParent); + node = new NodeItem<>("subject", parent); + //when + node.addChild(grandParent); + } + + /** + * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a + * child to a node, sets the child's parent node. + */ + @Test + public void shouldSetParentOnChildWhenAddedAsChild() { + //given + Node child = new NodeItem<>("child"); + node = new NodeItem<>("subject"); + //when + node.addChild(child); + //then + assertThat(child.getParent(), is(node)); + } + + /** + * Test {@link NodeItem#walkTree(java.util.List) } that we can walk a tree + * to the target node. + */ + @Test + public void shouldWalkTreeToNode() { + //given + final String grandparent = "grandparent"; + Node grandParentNode = new NodeItem<>(grandparent); + final String parent = "parent"; + Node parentNode = new NodeItem<>(parent, grandParentNode); + final String subject = "subject"; + node = new NodeItem<>(subject, parentNode); + //when + Optional> result = grandParentNode.walkTree(Arrays.asList( + parent, subject)); + //then + assertTrue(result.isPresent()); + assertThat(result.get(), is(node)); + } + + /** + * Test {@link NodeItem#walkTree(java.util.List) } that we get an empty + * {@link Optional} when walking a path that doesn't exist. + */ + @Test + public void shouldNotFindNonExistantChildNode() { + //given + final String parent = "parent"; + Node parentNode = new NodeItem<>(parent); + final String subject = "subject"; + node = new NodeItem<>(subject, parentNode); + //when + Optional> result = parentNode.walkTree(Arrays.asList( + subject, "no child")); + //then + assertFalse(result.isPresent()); + } + + /** + * Test {@link NodeItem#walkTree(java.util.List) } that when we pass null we + * get an exception. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNEWhenWalkTreeNull() { + //given + node = new NodeItem<>("subject"); + //when + node.walkTree(null); + } + + /** + * Test {@link NodeItem#walkTree(java.util.List) } that when we pass an + * empty path we get and empty {@link Optional} as a result. + */ + @Test + public void shouldReturnEmptyForEmptyWalkTreePath() { + //given + node = new NodeItem<>("subject"); + //when + node.walkTree(Collections.emptyList()); + } + + /** + * Test {@link NodeItem#createDescendantLine(java.util.List) } that we can + * create a chain of descendant nodes. + */ + @Test + public void shouldCreateDescendantNodes() { + //given + node = new NodeItem<>("subject"); + final String alphaData = "alpha"; + final String betaData = "beta"; + final String gammaData = "gamma"; + //when + node.createDescendantLine( + Arrays.asList(alphaData, betaData, gammaData)); + //then + final Optional> alphaOptional = node.getChild(alphaData); + assertTrue(alphaOptional.isPresent()); + Node alpha = alphaOptional.get(); + assertThat(alpha.getParent(), is(node)); + final Optional> betaOptional = alpha.getChild(betaData); + assertTrue(betaOptional.isPresent()); + Node beta = betaOptional.get(); + assertThat(beta.getParent(), is(alpha)); + final Optional> gammaOptional = beta.getChild(gammaData); + assertTrue(gammaOptional.isPresent()); + Node gamma = gammaOptional.get(); + assertThat(gamma.getParent(), is(beta)); + } + + /** + * Test {@link NodeItem#createDescendantLine(java.util.List) } that if we + * pass null to create a chain of descendant nodes we get an exception. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenCreateDescendantNull() { + //given + node = new NodeItem<>("subject"); + //when + node.createDescendantLine(null); + } + + /** + * Test {@link NodeItem#createDescendantLine(java.util.List) } that if we + * pass an empty list nothing is changed. + */ + @Test + public void shouldChangeNothingWhenCreateDescendantEmpty() { + //given + node = new NodeItem<>("subject"); + //when + node.createDescendantLine(Collections.emptyList()); + //then + assertThat(node.getChildren().size(), is(0)); + } + + /** + * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that we can + * find a child of a node. + */ + @Test + public void shouldFindExistingChildNode() { + //given + node = new NodeItem<>("subject"); + final String childData = "child"; + Node child = new NodeItem<>(childData, node); + //when + Node found = node.findOrCreateChild(childData); + //then + assertThat(found, is(child)); + } + + /** + * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that we create + * a missing child of a node. + */ + @Test + public void shouldFindCreateNewChildNode() { + //given + node = new NodeItem<>("subject"); + final String childData = "child"; + //when + Node found = node.findOrCreateChild(childData); + //then + assertThat(found.getData(), is(childData)); + } + + /** + * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that if we + * pass null we get an exception. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEFWhenFindOrCreateChildNull() { + //given + node = new NodeItem<>("subject"); + //when + node.findOrCreateChild(null); + } + + /** + * Test {@link NodeItem#getChild(java.lang.Object) } that we can get the + * node for a child. + */ + @Test + public void shouldGetChild() { + //given + node = new NodeItem<>("subject"); + final String childData = "child"; + Node child = new NodeItem<>(childData); + node.addChild(child); + //when + Optional> found = node.getChild(childData); + //then + assertTrue(found.isPresent()); + assertThat(found.get(), is(child)); + } + + /** + * Test {@link NodeItem#getChild(java.lang.Object) } that we throw an + * exception when passed null. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenGetChildNull() { + //given + node = new NodeItem<>("subject"); + //when + node.getChild(null); + } + + /** + * Test {@link NodeItem#createChild(java.lang.Object) } that we create a + * child as a child of the current node and with the current node as its + * parent. + */ + @Test + public void shoudCreateChild() { + //given + node = new NodeItem<>("subject"); + final String childData = "child"; + //when + Node child = node.createChild(childData); + //then + assertThat(child.getParent(), is(node)); + final Optional> foundChild = node.getChild(childData); + assertTrue(foundChild.isPresent()); + assertThat(foundChild.get(), is(child)); + } + + /** + * Test that we throw an exception when passed null. + */ + @Test(expected = NullPointerException.class) + public void shouldThrowNPEWhenCreateChildNull() { + //given + node = new NodeItem<>("subject"); + //when + node.createChild(null); + } + +}