Merge pull request #15 from kemitix/immutable-tree

Immutable tree
This commit is contained in:
Paul Campbell 2016-08-21 19:16:33 +01:00 committed by GitHub
commit f0b2ccbbb4
5 changed files with 744 additions and 2 deletions

View file

@ -0,0 +1,157 @@
package net.kemitix.node;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* An abstract node item, providing default implementations for most read-only
* operations.
*
* @param <T> the type of data stored in each node
*
* @author pcampbell
*/
abstract class AbstractNodeItem<T> implements Node<T> {
private T data;
private String name;
private Node<T> parent;
private final Set<Node<T>> children;
protected AbstractNodeItem(
final T data, final String name, final Node<T> parent,
final Set<Node<T>> children) {
this.data = data;
this.name = name;
this.parent = parent;
this.children = children;
}
@Override
public String getName() {
return name;
}
@Override
public Optional<T> getData() {
return Optional.ofNullable(data);
}
@Override
public boolean isEmpty() {
return data == null;
}
@Override
public Optional<Node<T>> getParent() {
return Optional.ofNullable(parent);
}
@Override
public Set<Node<T>> getChildren() {
return new HashSet<>(children);
}
/**
* 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<Node<T>> findChild(final T child) {
if (child == null) {
throw new NullPointerException("child");
}
return children.stream().filter(node -> {
final Optional<T> d = node.getData();
return d.isPresent() && d.get().equals(child);
}).findAny();
}
@Override
public Node<T> getChild(final T child) {
return findChild(child).orElseThrow(
() -> new NodeException("Child not found"));
}
/**
* Checks if the node is an ancestor.
*
* @param node the potential ancestor
*
* @return true if the node is an ancestor
*/
@Override
public boolean isDescendantOf(final Node<T> node) {
return parent != null && (node.equals(parent) || parent.isDescendantOf(
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<Node<T>> findInPath(final List<T> path) {
if (path == null) {
throw new NullPointerException("path");
}
if (path.size() > 0) {
Optional<Node<T>> found = findChild(path.get(0));
if (found.isPresent()) {
if (path.size() > 1) {
return found.get().findInPath(path.subList(1, path.size()));
}
return found;
}
}
return Optional.empty();
}
@Override
public Optional<Node<T>> findChildByName(final String named) {
if (named == null) {
throw new NullPointerException("name");
}
return children.stream()
.filter(n -> n.getName().equals(named))
.findAny();
}
@Override
public Node<T> getChildByName(final String named) {
return findChildByName(named).orElseThrow(
() -> 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().forEach(c -> sb.append(c.drawTree(depth + 1)));
return sb.toString();
}
@Override
public boolean isNamed() {
return name != null && name.length() > 0;
}
}

View file

@ -0,0 +1,95 @@
package net.kemitix.node;
import java.util.List;
import java.util.Set;
/**
* Represents an immutable tree of nodes.
*
* <p>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.</p>
*
* @param <T> the type of data stored in each node
*
* @author pcampbell
*/
final class ImmutableNodeItem<T> extends AbstractNodeItem<T> {
private static final String IMMUTABLE_OBJECT = "Immutable object";
private ImmutableNodeItem(
final T data, final String name, final Node<T> parent,
final Set<Node<T>> children) {
super(data, name, parent, children);
}
static <T> ImmutableNodeItem<T> newRoot(
final T data, final String name, final Set<Node<T>> children) {
return new ImmutableNodeItem<>(data, name, null, children);
}
static <T> ImmutableNodeItem<T> newChild(
final T data, final String name, final Node<T> parent,
final Set<Node<T>> 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<T> parent) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public void addChild(final Node<T> child) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public Node<T> createChild(final T child) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public Node<T> createChild(final T child, final String name) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public void createDescendantLine(final List<T> descendants) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public Node<T> findOrCreateChild(final T child) {
return findChild(child).orElseThrow(
() -> new UnsupportedOperationException(IMMUTABLE_OBJECT));
}
@Override
public void insertInPath(final Node<T> node, final String... path) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public void removeChild(final Node<T> node) {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
@Override
public void removeParent() {
throw new UnsupportedOperationException(IMMUTABLE_OBJECT);
}
}

View file

@ -14,7 +14,7 @@ import java.util.function.Function;
*
* @author pcampbell
*/
public class NodeItem<T> implements Node<T> {
class NodeItem<T> implements Node<T> {
private T data;

View file

@ -1,5 +1,9 @@
package net.kemitix.node;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Utility class for {@link Node} items.
*
@ -63,4 +67,41 @@ public final class Nodes {
return new NodeItem<>(data, name, parent);
}
/**
* Creates an immutable copy of an existing node tree.
*
* @param root the root node of the source tree
* @param <T> the type of the data
*
* @return the immutable copy of the tree
*/
public static <T> Node<T> asImmutable(final Node<T> root) {
if (root.getParent().isPresent()) {
throw new IllegalArgumentException("source must be the root node");
}
final Set<Node<T>> children = getImmutableChildren(root);
return ImmutableNodeItem.newRoot(root.getData().orElse(null),
root.getName(), children);
}
private static <T> Set<Node<T>> getImmutableChildren(final Node<T> source) {
return source.getChildren()
.stream()
.map(Nodes::asImmutableChild)
.collect(Collectors.toSet());
}
private static <T> Node<T> asImmutableChild(
final Node<T> source) {
final Optional<Node<T>> sourceParent = source.getParent();
if (sourceParent.isPresent()) {
return ImmutableNodeItem.newChild(source.getData().orElse(null),
source.getName(), sourceParent.get(),
getImmutableChildren(source));
} else {
throw new IllegalArgumentException(
"source must not be the root node");
}
}
}

View file

@ -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<String> 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<Node<String>> 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");
}
}