From cd598dc46b673b3829d3cd2e4ccf19eebd2c54b7 Mon Sep 17 00:00:00 2001 From: Greg Gibeling Date: Sat, 5 Oct 2024 09:54:35 -0700 Subject: [PATCH] G2-1654 Trie Validation --- .../com/g2forge/alexandria/adt/trie/Node.java | 88 ++++++++++++++++++- .../g2forge/alexandria/adt/trie/TestNode.java | 33 +++++++ .../alexandria/adt/trie/TestTrie3Node.java | 9 +- .../alexandria/adt/trie/TestTrie5Node.java | 7 +- 4 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNode.java diff --git a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java index 7581d706..0cbd2368 100644 --- a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java +++ b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java @@ -1,20 +1,73 @@ package com.g2forge.alexandria.adt.trie; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import com.g2forge.alexandria.java.validate.CompositeValidation; +import com.g2forge.alexandria.java.validate.IValidatable; +import com.g2forge.alexandria.java.validate.IValidation; import com.g2forge.alexandria.path.path.IPath; import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.ToString; @Getter @AllArgsConstructor @ToString @EqualsAndHashCode -public class Node { +public class Node implements IValidatable { + @Data + @Builder(toBuilder = true) + @RequiredArgsConstructor + public static class ChildLabelValidation implements IValidation { + protected final KT key; + + protected final IPath label; + + @Override + public boolean isValid() { + return getLabel() != null && getKey().equals(getLabel().getFirst()); + } + } + + @Data + @Builder(toBuilder = true) + @RequiredArgsConstructor + public static class EmptyValueValidation implements IValidation { + protected final boolean isTerminal; + + protected final V value; + + @Override + public boolean isValid() { + return isTerminal() || getValue() == null; + } + } + + @Data + @Builder(toBuilder = true) + @RequiredArgsConstructor + public static class NonCircularValidation implements IValidation { + protected final Map, ?> ancestors; + + protected final Node node; + + @Override + public boolean isValid() { + return !getAncestors().containsKey(getNode()); + } + } + protected final IPath label; protected final Map> children; @@ -30,4 +83,37 @@ public Node(IPath label) { public Node(IPath label, V value) { this(label, new HashMap<>(), true, value); } + + @Override + public IValidation validate() { + final Object value = new Object(); + final IdentityHashMap, Object> ancestors = new IdentityHashMap<>(); + final CompositeValidation.CompositeValidationBuilder retVal = CompositeValidation.builder(); + + final LinkedList> queue = new LinkedList>(); + queue.add(this); + while (!queue.isEmpty()) { + final Node current = queue.removeFirst(); + // See if we're done with all the descendants of the current node + if (ancestors.remove(current) != null) continue; + ancestors.put(current, value); + + retVal.validation(new EmptyValueValidation<>(isTerminal(), getValue())); + + final List> children = new ArrayList<>(); + for (Map.Entry> entry : current.getChildren().entrySet()) { + retVal.validation(new ChildLabelValidation<>(entry.getKey(), entry.getValue().getLabel())); + final NonCircularValidation nonCircularValidation = new NonCircularValidation<>(new IdentityHashMap<>(ancestors), entry.getValue()); + retVal.validation(nonCircularValidation); + if (nonCircularValidation.isValid()) children.add(entry.getValue()); + } + + // Queue the children, but queue this node first to mark the end of the children + Collections.reverse(children); + queue.addFirst(current); + children.stream().forEach(queue::addFirst); + } + + return retVal.build(); + } } diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNode.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNode.java new file mode 100644 index 00000000..3185b708 --- /dev/null +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNode.java @@ -0,0 +1,33 @@ +package com.g2forge.alexandria.adt.trie; + +import java.util.HashMap; + +import org.junit.Test; + +import com.g2forge.alexandria.java.validate.IValidation; +import com.g2forge.alexandria.path.path.Path; +import com.g2forge.alexandria.test.HAssert; + +public class TestNode { + @Test + public void terminalValue() { + final IValidation validation = new Node(null, new HashMap<>(), false, new Object()).validate(); + HAssert.assertFalse(validation.isValid()); + } + + @Test + public void childLabel() { + final Node root = new Node(null); + root.getChildren().put("a", new Node<>(new Path<>("b"))); + final IValidation validation = root.validate(); + HAssert.assertFalse(validation.isValid()); + } + + @Test + public void circular() { + final Node root = new Node(null); + root.getChildren().put("a", root); + final IValidation validation = root.validate(); + HAssert.assertFalse(validation.isValid()); + } +} diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java index 627903ff..d69048bc 100644 --- a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java @@ -6,11 +6,16 @@ import com.g2forge.alexandria.test.HAssert; public class TestTrie3Node { - protected static final ITrie trie = new Trie<>(new NodeBuilder("t", null).children(c -> { + protected static final Trie trie = new Trie<>(new NodeBuilder("t", null).children(c -> { c.child("est", "test"); c.child("oast", "toast"); }).build()); + @Test + public void validate() { + HAssert.assertTrue(trie.root.validate().isValid()); + } + @Test public void roast() { final IOptional result = trie.get(NodeBuilder.toLabel("roast")); @@ -48,7 +53,7 @@ public void toasting() { final IOptional result = trie.get(NodeBuilder.toLabel("toasting")); HAssert.assertFalse(result.isNotEmpty()); } - + @Test public void toasti() { final IOptional result = trie.get(NodeBuilder.toLabel("toasti")); diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java index 40661414..e18d89d1 100644 --- a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java @@ -6,7 +6,7 @@ import com.g2forge.alexandria.test.HAssert; public class TestTrie5Node { - protected static final ITrie trie = new Trie<>(new NodeBuilder("t", null).children(c0 -> { + protected static final Trie trie = new Trie<>(new NodeBuilder("t", null).children(c0 -> { c0.child("est", "test"); c0.child("oast", "toast").children(c1 -> { c1.child("er", "toaster"); @@ -65,4 +65,9 @@ public void trip() { final IOptional result = trie.get(NodeBuilder.toLabel("trip")); HAssert.assertFalse(result.isNotEmpty()); } + + @Test + public void validate() { + HAssert.assertTrue(trie.root.validate().isValid()); + } }