diff --git a/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/Node.java b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/CTrie.java similarity index 81% rename from vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/Node.java rename to vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/CTrie.java index 73e7307..3387d75 100644 --- a/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/Node.java +++ b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/CTrie.java @@ -1,13 +1,14 @@ package io.vertx.web.sync.impl; -import io.vertx.web.sync.WebHandler; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; import java.util.ArrayList; import java.util.List; -import static io.vertx.web.sync.impl.Node.Type.*; +import static io.vertx.web.sync.impl.CTrie.Type.*; -public class Node { +public class CTrie { enum Type { STATIC, @@ -33,22 +34,22 @@ private static int countParams(String path) { private Type type; private int maxParams; private String indices; - private List children; - private WebHandler[] handle; + private List> children; + private LList data; private int priority; - Node() { + public CTrie() { this("", false, STATIC, 0, "", new ArrayList<>(), null, 0); } - private Node(String path, boolean wildChild, Type type, int maxParams, String indices, List children, WebHandler[] handle, int priority) { + private CTrie(String path, boolean wildChild, Type type, int maxParams, String indices, List> children, LList data, int priority) { this.path = path; this.wildChild = wildChild; this.type = type; this.maxParams = maxParams; this.indices = indices; this.children = children; - this.handle = handle; + this.data = data; this.priority = priority; } @@ -59,7 +60,7 @@ private int addPriority(int pos) { // Adjust position (move to from) int newPos = pos; while (newPos > 0 && children.get(newPos - 1).priority < prio) { - final Node temp = children.get(newPos); + final CTrie temp = children.get(newPos); children.set(newPos, children.get(newPos - 1)); children.set(newPos - 1, temp); newPos--; @@ -77,8 +78,8 @@ private int addPriority(int pos) { return newPos; } - private void insertChild(int numParams, String path, String fullPath, WebHandler[] handle) { - Node n = this; + private void insertChild(int numParams, String path, String fullPath, T handle) { + CTrie n = this; int offset = 0; // Already handled chars of the path // Find prefix until first wildcard @@ -116,7 +117,7 @@ private void insertChild(int numParams, String path, String fullPath, WebHandler offset = i; } - final Node child = new Node("", false, PARAM, numParams, "", new ArrayList<>(), null, 0); + final CTrie child = new CTrie<>("", false, PARAM, numParams, "", new ArrayList<>(), null, 0); n.children = new ArrayList<>(); n.children.add(child); n.wildChild = true; @@ -127,7 +128,7 @@ private void insertChild(int numParams, String path, String fullPath, WebHandler n.path = path.substring(offset, end); offset = end; - final Node staticChild = new Node( + final CTrie staticChild = new CTrie<>( "", false, STATIC, @@ -158,7 +159,7 @@ private void insertChild(int numParams, String path, String fullPath, WebHandler n.path = path.substring(offset, i); // first node: catchAll node with empty path - final Node catchAllChild = new Node("", true, CATCH_ALL, 1, "", new ArrayList<>(), null, 0); + final CTrie catchAllChild = new CTrie<>("", true, CATCH_ALL, 1, "", new ArrayList<>(), null, 0); n.children = new ArrayList<>(); n.children.add(catchAllChild); n.indices = path.substring(i, i + 1); @@ -166,14 +167,14 @@ private void insertChild(int numParams, String path, String fullPath, WebHandler n.priority++; // second node: node holding the variable - final Node child = new Node( + final CTrie child = new CTrie<>( path.substring(i), false, CATCH_ALL, 1, "", new ArrayList<>(), - handle, + new LList<>(handle), 1 ); n.children = new ArrayList<>(); @@ -185,12 +186,12 @@ private void insertChild(int numParams, String path, String fullPath, WebHandler // insert remaining path part and handle to the leaf n.path = path.substring(offset); - n.handle = handle; + n.data = new LList<>(handle); } - public void addRoute(String path, WebHandler... handle) { - Node n = this; + public void add(String path, T handle) { + CTrie n = this; String fullPath = path; n.priority++; @@ -216,14 +217,14 @@ public void addRoute(String path, WebHandler... handle) { // Split edge if (i < n.path.length()) { - final Node child = new Node( + final CTrie child = new CTrie<>( n.path.substring(i), n.wildChild, STATIC, 0, n.indices, n.children, - n.handle, + n.data, n.priority - 1 ); @@ -238,7 +239,7 @@ public void addRoute(String path, WebHandler... handle) { n.children.add(child); n.indices = n.path.substring(i, i + 1); n.path = path.substring(0, i); - n.handle = null; + n.data = null; n.wildChild = false; } @@ -298,7 +299,7 @@ public void addRoute(String path, WebHandler... handle) { // Otherwise insert it if (c != ':' && c != '*') { n.indices += c; - final Node child = new Node( + final CTrie child = new CTrie<>( "", false, STATIC, @@ -316,10 +317,12 @@ public void addRoute(String path, WebHandler... handle) { return; } else if (i == path.length()) { // Make node a (in-path leaf) - if (n.handle != null) { - throw new IllegalStateException("A handle is already registered for path '" + fullPath + "'"); + if (n.data != null) { + System.out.println("A handle is already registered for path '" + fullPath + "'"); + n.data.push(handle); + } else { + n.data = new LList<>(handle); } - n.handle = handle; } return; } @@ -332,22 +335,22 @@ public void addRoute(String path, WebHandler... handle) { } - public WebHandler[] search(final RoutingContextInternal ctx) { + public LList search(final RoutingContextInternal ctx) { return search(ctx, true); } - public WebHandler[] lookup(final RoutingContextInternal ctx) { + public LList lookup(final RoutingContextInternal ctx) { return search(ctx, false); } - private WebHandler[] search(final RoutingContextInternal ctx, boolean updateParams) { + private LList search(final RoutingContextInternal ctx, boolean updateParams) { String path = ctx.path(); - Node n = this; + CTrie n = this; walk: while (true) { if (path.length() > n.path.length()) { - if (path.substring(0, n.path.length()).equals(n.path)) { + if (path.startsWith(n.path)) { path = path.substring(n.path.length()); // If this node does not have a wildcard child, // we can just look up the next child node and continue @@ -392,35 +395,62 @@ private WebHandler[] search(final RoutingContextInternal ctx, boolean updatePara return null; } - return n.handle; + return n.data; case CATCH_ALL: if (updateParams) { ctx.addParam(n.path.substring(2), path); } - return n.handle; + return n.data; default: throw new RuntimeException("invalid node type"); } } } else if (path.equals(n.path)) { - return n.handle; + return n.data; } return null; } } - private void printTree(String prefix) { - System.out.println(" " + priority + ":" + maxParams + " " + prefix + path + "[" + children.size() + "] " + handle + " " + wildChild + " " + type); + public JsonObject toJson() { + final JsonObject json = new JsonObject() + .put("path", path) + .put("wildChild", wildChild) + .put("type", type) + .put("maxParams", maxParams) + .put("indices", indices); + + if (children != null) { + JsonArray arr = new JsonArray(); + json.put("children", arr); + for (CTrie trie : children) { + arr.add(trie.toJson()); + } + } - for (int l = path.length(); l > 0; l--) { - prefix += " "; + if (data != null) { + JsonArray arr = new JsonArray(); + json.put("data", arr); + data.forEach(el -> { + arr.add(el.toString()); + }); } - for (Node child : children) { - child.printTree(prefix); + json.put("priority", priority); + + return json; + } + + private void printTree(String prefix) { + System.out.println(" " + priority + ":" + maxParams + " " + prefix + path + "[" + children.size() + "] " + data + " " + wildChild + " " + type); + + String indent = " ".repeat(path.length()); + + for (CTrie child : children) { + child.printTree(indent); } } diff --git a/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/LList.java b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/LList.java new file mode 100644 index 0000000..8aec669 --- /dev/null +++ b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/LList.java @@ -0,0 +1,68 @@ +package io.vertx.web.sync.impl; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class LList { + + Node head; + Node tail; + + // Linked list Node. + static class Node { + T data; + Node next; + + // Constructor + Node(T d) { + data = d; + next = null; + } + } + + LList(T data) { + push(data); + } + + LList() { + } + + public void push(T data) { + // Create a new node with given data + Node new_node = new Node<>(data); + + // If the Linked List is empty, + // then make the new node as head + if (head == null) { + head = new_node; + } else { + // Insert the new_node at tail node + tail.next = new_node; + } + // update tail + tail = new_node; + } + + public void forEach(Function consumer) { + Node currNode = head; + // Traverse + while (currNode != null) { + if (consumer.apply(currNode.data)) { + // Go to next node + currNode = currNode.next; + } else { + return; + } + } + } + + public void forEach(Consumer consumer) { + Node currNode = head; + // Traverse + while (currNode != null) { + consumer.accept(currNode.data); + // Go to next node + currNode = currNode.next; + } + } +} diff --git a/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/RouterImpl.java b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/RouterImpl.java index 51b1f4f..1627b32 100644 --- a/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/RouterImpl.java +++ b/vertx-web-sync-incubator/src/main/java/io/vertx/web/sync/impl/RouterImpl.java @@ -6,13 +6,15 @@ import io.vertx.web.sync.RouterOptions; import io.vertx.web.sync.WebHandler; +import java.util.Arrays; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; public class RouterImpl implements Router { private final RouterOptions opts; - private final Map trees; + private final Map> trees; public RouterImpl(RouterOptions opts) { if (opts.getPrefix() != null && opts.getPrefix().charAt(0) != '/') { @@ -30,21 +32,24 @@ public Router on(HttpMethod method, String path, WebHandler... handlers) { } if (!trees.containsKey(method)) { - trees.put(method, new Node()); + trees.put(method, new CTrie<>()); } if (opts.getPrefix() != null) { path = opts.getPrefix() + path; } - trees.get(method).addRoute(path, handlers); + final CTrie values = trees.get(method); + for (WebHandler h : handlers) { + values.add(path, h); + } return this; } - private WebHandler[] find(RoutingContextInternal ctx) { + private LList find(RoutingContextInternal ctx) { final HttpMethod verb = ctx.method(); - final Node tree = trees.get(verb); + final CTrie tree = trees.get(verb); if (tree != null) { return tree.search(ctx); } @@ -56,7 +61,7 @@ private WebHandler[] find(RoutingContextInternal ctx) { public void handle(HttpServerRequest req) { final RoutingContextImpl ctx = new RoutingContextImpl(req); - final WebHandler[] needle = find(ctx); + final LList needle = find(ctx); if (needle == null) { // final Handler handle405 = opts.getMethodNotAllowedHandler(); @@ -78,20 +83,21 @@ public void handle(HttpServerRequest req) { // } // } } else { - for (WebHandler handler : needle) { + needle.forEach(handler -> { try { switch (handler.handle(ctx)) { case NEXT: - continue; + return true; case END: - return; + return false; default: throw new IllegalStateException("Not Implemented"); } } catch (RuntimeException e) { e.printStackTrace(); + return false; } - } + }); } } } diff --git a/vertx-web-sync-incubator/src/test/java/io/vertx/web/sync/CTrieTest.java b/vertx-web-sync-incubator/src/test/java/io/vertx/web/sync/CTrieTest.java new file mode 100644 index 0000000..f110f09 --- /dev/null +++ b/vertx-web-sync-incubator/src/test/java/io/vertx/web/sync/CTrieTest.java @@ -0,0 +1,242 @@ +package io.vertx.web.sync; + +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.web.sync.impl.CTrie; +import io.vertx.web.sync.impl.LList; +import io.vertx.web.sync.impl.RoutingContextInternal; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + +public class CTrieTest { + + static class TestNeedle implements RoutingContextInternal { + + public final String path; + + public final MultiMap params = MultiMap.caseInsensitiveMultiMap(); + public final MultiMap verify = MultiMap.caseInsensitiveMultiMap(); + + TestNeedle(String path) { + this.path = path; + } + + public TestNeedle test(String name, String value) { + this.verify.add(name, value); + return this; + } + + @Override + public void addParam(String name, String value) { + this.params.add(name, value); + } + + @Override + public String path() { + return path; + } + + @Override + public HttpMethod method() { + return null; + } + + @Override + public String getHeader(CharSequence key) { + return null; + } + + @Override + public RoutingContext putHeader(CharSequence key, CharSequence value) { + return null; + } + + @Override + public WebHandler.HandlerReturn next() { + return null; + } + + @Override + public WebHandler.HandlerReturn end() { + return null; + } + + @Override + public WebHandler.HandlerReturn end(String chunk) { + return null; + } + + @Override + public WebHandler.HandlerReturn end(Buffer chunk) { + return null; + } + } + + + final Handler noOp = (x) -> { + }; + final Handler filterNoOp = (x) -> { + }; + + @Test + public void testAddAndGet() { + final CTrie> tree = new CTrie<>(); + + final String[] routes = { + "/hi", + "/contact", + "/co", + "/c", + "/a", + "/ab", + "/doc/", + "/doc/node_faq.html", + "/doc/node1.html", + "/α", + "/β" + }; + + for (String route : routes) { + tree.add(route, noOp); + } + + final String[] goodTestData = { + "/a", + "/hi", + "/contact", + "/co", + "/ab", + "/α", + "/β" + }; + + final String[] badTestData = { + "/", + "/con", + "/cone", + "/no" + }; + + for (String route : goodTestData) { + final LList> needle = tree.search(new TestNeedle(route)); + assertNotNull(needle); + } + + for (String route : badTestData) { + final LList> needle = tree.search(new TestNeedle(route)); + assertNull(needle); + } + } + + @Test + public void testWildcard() { + final CTrie> tree = new CTrie<>(); + final String[] routes = { + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/node_faq.html", + "/doc/node1.html", + "/info/:user/public", + "/info/:user/project/:project" + }; + + for (String route : routes) { + tree.add(route, noOp); + } + + // tree.printTree(); + + final TestNeedle[] foundData = { + new TestNeedle("/"), + new TestNeedle("/cmd/test/").test("tool", "test"), + new TestNeedle("/cmd/test/3").test("tool", "test").test("sub", "3"), + new TestNeedle("/src/").test("filepath", "/"), + new TestNeedle("/src/some/file.png").test("filepath", "/some/file.png"), + new TestNeedle("/search/"), + new TestNeedle("/search/中文").test("query", "中文"), + new TestNeedle("/user_noder").test("name", "noder"), + new TestNeedle("/user_noder/about").test("name", "noder"), + new TestNeedle("/files/js/inc/framework.js").test("dir", "js").test("filepath", "/inc/framework.js"), + new TestNeedle("/info/gordon/public").test("user", "gordon"), + new TestNeedle("/info/gordon/project/node").test("user", "gordon").test("project", "node") + }; + + for (TestNeedle testNeedle : foundData) { + final LList> needle = tree.search(testNeedle); + assertNotNull(needle); + // TODO: properly compare the multimap + assertEquals(testNeedle.params.toString(), testNeedle.verify.toString()); + } + + final TestNeedle[] noHandlerData = { + new TestNeedle("/cmd/test").test("tool", "test"), + new TestNeedle("/search/中文/").test("query", "中文") + }; + + for (TestNeedle testNeedle : noHandlerData) { + final LList> needle = tree.search(testNeedle); + assertNull(needle); + // TODO: properly compare the multimap + assertEquals(testNeedle.params.toString(), testNeedle.verify.toString()); + } + } + + @Test + public void testAppendHandler() { + final CTrie> tree = new CTrie<>(); + + // OK + tree.add("/", noOp); + // Should merge into a new list + tree.add("/", noOp); + + LList> needle = tree.search(new TestNeedle("/")); + assertNotNull(needle); + final AtomicInteger cnt = new AtomicInteger(); + needle.forEach(el -> { + assertEquals(noOp, el); + cnt.incrementAndGet(); + }); + assertEquals(2, cnt.get()); + } + + @Test + public void jsonTest() { + final CTrie> tree = new CTrie<>(); + final String[] routes = { + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/node_faq.html", + "/doc/node1.html", + "/info/:user/public", + "/info/:user/project/:project" + }; + + for (String route : routes) { + tree.add(route, noOp); + } + + System.out.println(tree.toJson().encodePrettily()); + } +}