diff --git a/src/main/java/net/kemitix/node/ImmutableNodeItem.java b/src/main/java/net/kemitix/node/ImmutableNodeItem.java new file mode 100644 index 0000000..7b80a59 --- /dev/null +++ b/src/main/java/net/kemitix/node/ImmutableNodeItem.java @@ -0,0 +1,95 @@ +package net.kemitix.node; + +import java.util.List; +import java.util.Set; + +/** + * Represents an immutable tree of nodes. + * + *

Due to the use of generics the data within a node may not be immutable. + * (We can't create a defensive copy.) So if a user were to use {@code + * getData()} they could then modify the original data within the node. This + * wouldn't affect the integrity of the node tree structure, however.

+ * + * @param the type of data stored in each node + * + * @author pcampbell + */ +final class ImmutableNodeItem extends AbstractNodeItem { + + private static final String IMMUTABLE_OBJECT = "Immutable object"; + + private ImmutableNodeItem( + final T data, final String name, final Node parent, + final Set> children) { + super(data, name, parent, children); + } + + static ImmutableNodeItem newRoot( + final T data, final String name, final Set> children) { + return new ImmutableNodeItem<>(data, name, null, children); + } + + static ImmutableNodeItem newChild( + final T data, final String name, final Node parent, + final Set> children) { + return new ImmutableNodeItem<>(data, name, parent, children); + } + + @Override + public void setName(final String name) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void setData(final T data) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void setParent(final Node parent) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void addChild(final Node child) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public Node createChild(final T child) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public Node createChild(final T child, final String name) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void createDescendantLine(final List descendants) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public Node findOrCreateChild(final T child) { + return findChild(child).orElseThrow( + () -> new UnsupportedOperationException(IMMUTABLE_OBJECT)); + } + + @Override + public void insertInPath(final Node node, final String... path) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void removeChild(final Node node) { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + + @Override + public void removeParent() { + throw new UnsupportedOperationException(IMMUTABLE_OBJECT); + } + +} diff --git a/src/test/java/net/kemitix/node/ImmutableNodeItemTest.java b/src/test/java/net/kemitix/node/ImmutableNodeItemTest.java new file mode 100644 index 0000000..9475ff5 --- /dev/null +++ b/src/test/java/net/kemitix/node/ImmutableNodeItemTest.java @@ -0,0 +1,449 @@ +package net.kemitix.node; + +import lombok.val; +import org.assertj.core.api.SoftAssertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; + +/** + * Test for {@link ImmutableNodeItem}. + * + * @author pcampbell + */ +public class ImmutableNodeItemTest { + + private static final String IMMUTABLE_OBJECT = "Immutable object"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private Node immutableNode; + + private void expectImmutableException() { + exception.expect(UnsupportedOperationException.class); + exception.expectMessage(IMMUTABLE_OBJECT); + } + + @Test + public void getDataReturnsData() { + //given + val data = "this immutableNode data"; + //when + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(data)); + //then + assertThat(immutableNode.getData()).as( + "can get the data from a immutableNode"). + contains(data); + } + + @Test + public void canCreateAnEmptyAndUnnamedNode() { + //when + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + //then + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(immutableNode.isEmpty()) + .as("immutableNode is empty") + .isTrue(); + softly.assertThat(immutableNode.isNamed()) + .as("immutableNode is unnamed") + .isFalse(); + softly.assertAll(); + } + + @Test + public void shouldThrowExceptionOnSetName() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + expectImmutableException(); + //when + immutableNode.setName("named"); + } + + @Test + public void rootNodeShouldHaveNoParent() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("data")); + //then + assertThat(immutableNode.getParent()).as( + "immutableNode created without a parent has no parent") + .isEmpty(); + } + + @Test + public void shouldContainImmutableCopyOfChild() { + //given + val parent = Nodes.unnamedRoot("root"); + val child = Nodes.namedChild("child", "child", parent); + //when + immutableNode = Nodes.asImmutable(parent); + //then + val immutableChild = immutableNode.getChildByName("child"); + assertThat(immutableChild).isNotSameAs(child); + assertThat(immutableChild.getName()).isEqualTo("child"); + } + + @Test + public void childShouldHaveImmutableParent() { + //given + val parent = Nodes.namedRoot("parent", "root"); + Nodes.namedChild("subject", "child", parent); + //when + immutableNode = Nodes.asImmutable(parent); + //then + // get the immutable node's child's parent + val immutableChild = immutableNode.getChildByName("child"); + final Optional> optionalParent + = immutableChild.getParent(); + if (optionalParent.isPresent()) { + val p = optionalParent.get(); + assertThat(p).hasFieldOrPropertyWithValue("name", "root") + .hasFieldOrPropertyWithValue("data", + Optional.of("parent")); + } + } + + @Test + public void shouldNotBeAbleToAddChildToImmutableTree() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("root")); + expectImmutableException(); + //when + Nodes.unnamedChild("child", immutableNode); + } + + @Test + public void shouldThrowExceptionWhenSetParent() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("subject")); + expectImmutableException(); + //when + immutableNode.setParent(null); + } + + @Test + public void shouldThrowExceptionWhenAddingChild() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("subject")); + expectImmutableException(); + //when + immutableNode.addChild(Nodes.unnamedRoot("child")); + } + + /** + * Test that we can walk a tree to the target node. + */ + @Test + public void shouldWalkTreeToNode() { + //given + val root = Nodes.unnamedRoot("root"); + Nodes.namedChild("child", "child", Nodes.unnamedChild("parent", root)); + immutableNode = Nodes.asImmutable(root); + //when + val result = immutableNode.findInPath(Arrays.asList("parent", "child")); + //then + assertThat(result.isPresent()).isTrue(); + if (result.isPresent()) { + assertThat(result.get().getName()).isEqualTo("child"); + } + } + + /** + * Test that we get an empty {@link Optional} when walking a path that + * doesn't exist. + */ + @Test + public void shouldNotFindNonExistentChildNode() { + //given + val root = Nodes.unnamedRoot("root"); + Nodes.unnamedChild("child", Nodes.unnamedChild("parent", root)); + immutableNode = Nodes.asImmutable(root); + //when + val result = immutableNode.findInPath( + Arrays.asList("parent", "no child")); + //then + assertThat(result.isPresent()).isFalse(); + } + + /** + * Test that when we pass null we get an exception. + */ + @Test + public void shouldThrowNEWhenWalkTreeNull() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("subject")); + exception.expect(NullPointerException.class); + exception.expectMessage("path"); + //when + immutableNode.findInPath(null); + } + + /** + * Test that when we pass an empty path we get and empty {@link Optional} as + * a result. + */ + @Test + public void shouldReturnEmptyForEmptyWalkTreePath() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("subject")); + //when + val result = immutableNode.findInPath(Collections.emptyList()); + //then + assertThat(result).isEmpty(); + } + + /** + * Test that we can find a child of a immutableNode. + */ + @Test + public void shouldFindExistingChildNode() { + //given + val root = Nodes.unnamedRoot("root"); + Nodes.unnamedChild("child", root); + immutableNode = Nodes.asImmutable(root); + //when + val result = immutableNode.findChild("child"); + //then + assertThat(result.isPresent()).isTrue(); + if (result.isPresent()) { + assertThat(result.get().getData()).contains("child"); + } + } + + /** + * Test that if we pass null we get an exception. + */ + @Test + public void findOrCreateChildShouldThrowNPEFWhenChildIsNull() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("subject")); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); + //when + immutableNode.findOrCreateChild(null); + } + + /** + * Test that we throw an exception when passed null. + */ + @Test + public void getChildShouldThrowNPEWhenThereIsNoChild() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("data")); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); + //when + immutableNode.findChild(null); + } + + @Test + public void getChildNamedFindsChild() { + //given + val root = Nodes.namedRoot("root data", "root"); + val alpha = Nodes.namedRoot("alpha data", "alpha"); + val beta = Nodes.namedRoot("beta data", "beta"); + root.addChild(alpha); + root.addChild(beta); + immutableNode = Nodes.asImmutable(root); + //when + val result = immutableNode.getChildByName("alpha"); + //then + assertThat(result.getName()).isEqualTo(alpha.getName()); + } + + @Test + public void getChildNamedFindsNothing() { + //given + val root = Nodes.namedRoot("root data", "root"); + val alpha = Nodes.namedRoot("alpha data", "alpha"); + val beta = Nodes.namedRoot("beta data", "beta"); + root.addChild(alpha); + root.addChild(beta); + exception.expect(NodeException.class); + exception.expectMessage("Named child not found"); + immutableNode = Nodes.asImmutable(root); + //when + immutableNode.getChildByName("gamma"); + } + + @Test + public void removingParentThrowsException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + expectImmutableException(); + //when + immutableNode.removeParent(); + } + + @Test + public void findChildNamedShouldThrowNPEWhenNameIsNull() { + //given + exception.expect(NullPointerException.class); + exception.expectMessage("name"); + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + //when + immutableNode.findChildByName(null); + } + + @Test + public void isNamedNull() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + //then + assertThat(immutableNode.isNamed()).isFalse(); + } + + @Test + public void isNamedEmpty() { + //given + immutableNode = Nodes.asImmutable(Nodes.namedRoot(null, "")); + //then + assertThat(immutableNode.isNamed()).isFalse(); + } + + @Test + public void isNamedNamed() { + //given + immutableNode = Nodes.asImmutable(Nodes.namedRoot(null, "named")); + //then + assertThat(immutableNode.isNamed()).isTrue(); + } + + @Test + public void removeChildThrowsExceptions() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + expectImmutableException(); + //then + immutableNode.removeChild(null); + } + + @Test + public void drawTreeIsCorrect() { + //given + val root = Nodes.namedRoot("root data", "root"); + val bob = Nodes.namedChild("bob data", "bob", root); + val alice = Nodes.namedChild("alice data", "alice", root); + Nodes.namedChild("dave data", "dave", alice); + Nodes.unnamedChild("bob's child's data", + bob); // has no name and no children so no included + val kim = Nodes.unnamedChild("kim data", root); // nameless mother + Nodes.namedChild("lucy data", "lucy", kim); + immutableNode = Nodes.asImmutable(root); + //when + val tree = immutableNode.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]"); + } + + @Test + public void setDataShouldThrowException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("initial")); + expectImmutableException(); + //when + immutableNode.setData("updated"); + } + + @Test + public void createChildThrowsException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot(null)); + expectImmutableException(); + //when + immutableNode.createChild("child data", "child name"); + } + + @Test + public void canGetChildWhenFound() { + //given + val root = Nodes.unnamedRoot("data"); + val child = Nodes.namedChild("child data", "child name", root); + immutableNode = Nodes.asImmutable(root); + //when + val found = immutableNode.getChild("child data"); + //then + assertThat(found.getName()).isEqualTo(child.getName()); + } + + @Test + public void canGetChildWhenNotFound() { + //given + exception.expect(NodeException.class); + exception.expectMessage("Child not found"); + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("data")); + //when + immutableNode.getChild("child data"); + } + + @Test + public void canSafelyHandleFindChildWhenAChildHasNoData() { + //given + val root = Nodes.unnamedRoot(""); + Nodes.unnamedChild(null, root); + immutableNode = Nodes.asImmutable(root); + //when + immutableNode.findChild("data"); + } + + @Test + public void createChildShouldThrowException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("")); + expectImmutableException(); + //when + immutableNode.createChild("child"); + } + + @Test + public void createDescendantLineShouldThrowException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("")); + expectImmutableException(); + //when + immutableNode.createDescendantLine( + Arrays.asList("child", "grandchild")); + } + + @Test + public void insertInPathShouldThrowException() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("")); + expectImmutableException(); + //when + immutableNode.insertInPath(null, ""); + } + + @Test + public void findOrCreateChildShouldReturnChildWhenChildIsFound() { + //given + val root = Nodes.unnamedRoot(""); + Nodes.namedChild("child", "child", root); + immutableNode = Nodes.asImmutable(root); + //when + val found = immutableNode.findOrCreateChild("child"); + assertThat(found).extracting(Node::getName).contains("child"); + } + + @Test + public void findOrCreateChildShouldThrowExceptionWhenChildNotFound() { + //given + immutableNode = Nodes.asImmutable(Nodes.unnamedRoot("")); + expectImmutableException(); + //when + immutableNode.findOrCreateChild("child"); + } +}