/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.http.server.uri; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static io.netty.handler.codec.http.HttpMethod.*; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Test; /** * @author jason * */ public class RouteTrieTest { private static final String DOCUMENT = "document"; private static final String REST_RESOURCE = "restResource"; private static final String SCRIPT = "script"; private static final String STYLESHEET = "stylesheet"; private static final String STATIC = "static"; RouteMatch result; @Before public void before() { result = null; } private String result(int index) { return "/result" + index; } private Map<String, String> makeMap(String...s) { assert s.length % 2 == 0; Map<String, String> map = new HashMap<>(); for (int i = 0; i < s.length; i += 2) { map.put(s[i], s[i + 1]); } return map; } private RouteTrie makeMixedUpTrie() { return new RouteTrie() .addRoute(new Route(GET, "/d3", DOCUMENT, "d3/index")) .addRoute(new Route(GET, "/chat", DOCUMENT, "chat/index")) // pretty normal to have a prefix to the API i think .addRoute(new Route(GET, "/rest/resource-type", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(GET, "/rest/resource-type/:id(\\d+)", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(POST, "/rest/resource-type", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(PUT, "/rest/resource-type/:id(\\d+)", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(DELETE, "/rest/resource-type/:id(\\d+)", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(PATCH, "/rest/resource-type/:id(\\d+)", REST_RESOURCE, "services/resource-type")) .addRoute(new Route(GET, "/rest/resource-other", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(GET, "/rest/resource-other/:id(\\d+)", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(POST, "/rest/resource-other", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(PUT, "/rest/resource-other/:id(\\d+)", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(DELETE, "/rest/resource-other/:id(\\d+)", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(PATCH, "/rest/resource-other/:id(\\d+)", REST_RESOURCE, "services/resource-other")) .addRoute(new Route(GET, "/rest/resource-otter", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(GET, "/rest/resource-otter/:id(\\d+)", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(POST, "/rest/resource-otter", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(PUT, "/rest/resource-otter/:id(\\d+)", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(DELETE, "/rest/resource-otter/:id(\\d+)", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(PATCH, "/rest/resource-otter/:id(\\d+)", REST_RESOURCE, "services/resource-otter")) .addRoute(new Route(GET, "/rest/resource-otter/:id(\\d+)/nipples", REST_RESOURCE, "services/resource-otter-nipples")) .addRoute(new Route(GET, "/rest/resource-otter/:id(\\d+)/:part", REST_RESOURCE, "services/resource-otter-parts")) .addRoute(new Route(GET, "/rest/:letters([A-Za-z]+)", REST_RESOURCE, "services/letter-echo")) // these are normally added automatically at the end so let's test em! .addRoute(new Route(GET, "/*path.css", STYLESHEET, "")) .addRoute(new Route(GET, "/*path.js", SCRIPT, "")) .addRoute(new Route(GET, "/*fallthrough", STATIC, "")); } @Test public void testMixedUp() { RouteTrie rt = makeMixedUpTrie().compress(); checkTrie(rt); // System.out.println(rt); // // // prebench // for (int i = 0; i < 10000; ++i) { // checkTrie(rt); // } // // long now = System.currentTimeMillis(); // for (int i = 0; i < 10000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); // // now = System.currentTimeMillis(); // for (int i = 0; i < 10000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); // // now = System.currentTimeMillis(); // for (int i = 0; i < 10000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); // // now = System.currentTimeMillis(); // for (int i = 0; i < 30000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); // // now = System.currentTimeMillis(); // for (int i = 0; i < 30000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); // // now = System.currentTimeMillis(); // for (int i = 0; i < 30000; ++i) { // checkTrie(rt); // } // System.out.println(System.currentTimeMillis() - now); } private void checkTrie(RouteTrie rt) { result = rt.find(GET, new URIMatch("/d3/")); assertResult(DOCUMENT, "d3/index"); result = rt.find(GET, new URIMatch("/d3")); assertResult(DOCUMENT, "d3/index"); result = rt.find(GET, new URIMatch("/chat/")); assertResult(DOCUMENT, "chat/index"); result = rt.find(GET, new URIMatch("/chat")); assertResult(DOCUMENT, "chat/index"); result = rt.find(GET, new URIMatch("/chat.html")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/chat/index.html")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/chat/index.js")); assertResult(SCRIPT, ""); result = rt.find(GET, new URIMatch("/something/else.html")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/something/else.css")); assertResult(STYLESHEET, ""); result = rt.find(GET, new URIMatch("/something/else.js")); assertResult(SCRIPT, ""); result = rt.find(GET, new URIMatch("/something/else")); assertResult(STATIC, ""); result = rt.find(PATCH, new URIMatch("/rest/resource-other/22")); assertResult(REST_RESOURCE, "services/resource-other"); assertThat(result.params.get("id"), is("22")); result = rt.find(GET, new URIMatch("/rest/resource-other/22")); assertResult(REST_RESOURCE, "services/resource-other"); result = rt.find(GET, new URIMatch("/rest/resource-other/a22")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/rest/resource-other/22a")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/rest/resource-other/22/and-more")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/rest/resource-otter/2332")); assertResult(REST_RESOURCE, "services/resource-otter"); assertThat(result.params.get("id"), is("2332")); result = rt.find(GET, new URIMatch("/rest/resource")); assertResult(REST_RESOURCE, "services/letter-echo"); assertThat(result.params.get("letters"), is("resource")); result = rt.find(GET, new URIMatch("/rest/resource-static")); assertResult(STATIC, ""); result = rt.find(GET, new URIMatch("/rest/resource-otter/2332/nipples")); assertResult(REST_RESOURCE, "services/resource-otter-nipples"); assertThat(result.params.get("id"), is("2332")); result = rt.find(GET, new URIMatch("/rest/resource-otter/2332/knees")); assertResult(REST_RESOURCE, "services/resource-otter-parts"); assertThat(result.params.get("part"), is("knees")); } private void assertResult(String resourceName, String mapping) { assertTrue(result.matched()); assertThat(result.resourceName(), is(resourceName)); assertThat(result.route.mapping(), is(mapping)); } private RouteTrie makeParameterMatchTrie() { return new RouteTrie() // admittedly not the best example .addRoute(new Route(GET, "/user/:id([a-z]-[\\d]{6})/picture", DOCUMENT, result(0))) .addRoute(new Route(GET, "/user/:name([\\w]+)/picture", DOCUMENT, result(1))) .addRoute(new Route(GET, "/this/:is/:the/best", DOCUMENT, result(2))) .addRoute(new Route(GET, "/this/:is/:the/*end", DOCUMENT, result(3))); } @Test public void testParameterMatching() { RouteTrie trie = makeParameterMatchTrie().compress(); RouteMatch result = trie.find(GET, new URIMatch("/user/a-123456/picture")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(0))); assertThat(result.params.get("id"), is("a-123456")); assertThat(result.params.size(), is(1)); assertThat(result.route.resolve(makeMap("id", "b-987654")), is("/user/b-987654/picture")); assertThat(result.route.resolve(makeMap("id", "c-098765")), is("/user/c-098765/picture")); result = trie.find(GET, new URIMatch("/user/jason/picture")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(1))); assertThat(result.params.get("name"), is("jason")); assertThat(result.params.size(), is(1)); assertThat(result.route.resolve(makeMap("name", "jason")), is("/user/jason/picture")); assertThat(result.route.resolve(makeMap("name", "test")), is("/user/test/picture")); result = trie.find(GET, new URIMatch("/this/time/it/is/personal")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(3))); assertThat(result.params.get("is"), is("time")); assertThat(result.params.get("the"), is("it")); assertThat(result.params.get("end"), is("is/personal")); assertThat(result.params.size(), is(3)); result = trie.find(GET, new URIMatch("/this/is/not-the/best")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(2))); assertThat(result.params.get("is"), is("is")); assertThat(result.params.get("the"), is("not-the")); assertThat(result.params.size(), is(2)); } private RouteTrie makeRouteTrie() { return new RouteTrie() .addRoute(new Route(POST, "/this/is", STATIC, result(0))) .addRoute(new Route(DELETE, "/this/isno", STATIC, result(-100))) .addRoute(new Route(PUT, "/this/isno", STATIC, result(-200))) .addRoute(new Route(GET, "/this/isno", STATIC, result(-300))) .addRoute(new Route(POST, "/this/isno", STATIC, result(-400))) .addRoute(new Route(GET, "/this/isnot", STATIC, result(-1))) .addRoute(new Route(GET, "/this/is/the/bomb", STATIC, result(1))) .addRoute(new Route(GET, "/this/is/the/bomberman", STATIC, result(1000))) .addRoute(new Route(GET, "/this/is/the/best", STATIC, result(2))) .addRoute(new Route(GET, "/this/is/the/best-around", STATIC, result(2000))) .addRoute(new Route(GET, "/this/is/the.dir/bomb", STATIC, result(1001))) .addRoute(new Route(GET, "/this/is/the.dir/bomberman", STATIC, result(1002))) .addRoute(new Route(GET, "/this/is/the.dir/best", STATIC, result(2001))) .addRoute(new Route(GET, "/this/is/the.dir/best-around", STATIC, result(2002))) .addRoute(new Route(GET, "/this/:is/:the/best", STATIC, result(3))) .addRoute(new Route(GET, "/this/:is/:the/*end", STATIC, result(4))) // just to validate the structure works in order, // these rules would basically eat up everything but since they're at // the end, they match but don't get involved .addRoute(new Route(GET, "/this/*islast_and_should_not_interfere", STATIC, result(4000))) .addRoute(new Route(GET, "/this/*islast_and_also_is_not_used", STATIC, result(5000))) // the idea here is that this should pick up ANYTHING that ends in .css and wasn't picked up by // anything before this. which implies that matching needs to ignore extensions generally? .addRoute(new Route(GET, "/some.directory/static.:ext", DOCUMENT, result(5998))) .addRoute(new Route(GET, "/some.directory/*path.css", DOCUMENT, result(5999))) .addRoute(new Route(GET, "/*path.css", DOCUMENT, result(6000))) .addRoute(new Route(GET, "/*path.:ext", STATIC, result(7000))); } private void testRouteTrie(RouteTrie trie) { RouteMatch result = trie.find(POST, new URIMatch("/this/is")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(0))); assertTrue(result.params.isEmpty()); result = trie.find(DELETE, new URIMatch("/this/isno")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(-100))); assertTrue(result.params.isEmpty()); result = trie.find(GET, new URIMatch("/this/isnot")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(-1))); assertTrue(result.params.isEmpty()); result = trie.find(GET, new URIMatch("/this/is")); assertThat(result, is(notNullValue())); assertThat(result.route, is(nullValue())); assertThat(result.routes, is(nullValue())); assertThat(result.params, is(nullValue())); result = trie.find(GET, new URIMatch("/this/is/the/bomb")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(1))); assertTrue(result.params.isEmpty()); result = trie.find(GET, new URIMatch("/this/is/the.dir/bomb")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(1001))); assertTrue(result.params.isEmpty()); result = trie.find(GET, new URIMatch("/this/is/the/best")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(2))); assertTrue(result.params.isEmpty()); result = trie.find(GET, new URIMatch("/this/works/the/best")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(3))); assertThat(result.params.get("is"), is("works")); assertThat(result.params.get("the"), is("the")); assertThat(result.params.size(), is(2)); result = trie.find(GET, new URIMatch("/this/makes/me/bees-knees/if-you-know/what-i-am-saying")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(4))); assertThat(result.params.get("is"), is("makes")); assertThat(result.params.get("the"), is("me")); assertThat(result.params.get("end"), is("bees-knees/if-you-know/what-i-am-saying")); assertThat(result.params.size(), is(3)); result = trie.find(GET, new URIMatch("/jason.css")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(6000))); assertThat(result.params.get("path"), is("jason")); assertThat(result.params.size(), is(1)); result = trie.find(GET, new URIMatch("/jason")); assertThat(result, is(notNullValue())); assertThat(result.route, is(nullValue())); assertThat(result.routes, is(nullValue())); assertThat(result.params, is(nullValue())); result = trie.find(GET, new URIMatch("/jason/made/this/work.css")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(6000))); assertThat(result.params.get("path"), is("jason/made/this/work")); assertThat(result.params.size(), is(1)); result = trie.find(GET, new URIMatch("/jason/made/this/work")); assertThat(result, is(notNullValue())); assertThat(result.route, is(nullValue())); assertThat(result.routes, is(nullValue())); assertThat(result.params, is(nullValue())); result = trie.find(GET, new URIMatch("/jason/made/this/work.cs1")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(STATIC)); assertThat(result.route.mapping(), is(result(7000))); assertThat(result.params.get("path"), is("jason/made/this/work")); assertThat(result.params.get("ext"), is("cs1")); assertThat(result.params.size(), is(2)); result = trie.find(GET, new URIMatch("/some.directory/file.css")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(5999))); assertThat(result.params.get("path"), is("file")); assertThat(result.params.size(), is(1)); result = trie.find(GET, new URIMatch("/some.directory/static.css")); assertThat(result, is(notNullValue())); assertThat(result.route.resourceName(), is(DOCUMENT)); assertThat(result.route.mapping(), is(result(5998))); assertThat(result.params.get("ext"), is("css")); assertThat(result.params.size(), is(1)); } @Test public void testUncompressed() { testRouteTrie(makeRouteTrie()); } @Test public void testCompressed() { RouteTrie trie = makeRouteTrie(); //System.out.println(trie); trie.compress(); //System.out.println(trie); testRouteTrie(trie); } @Test public void testSpecialMethodHandling() { RouteTrie trie = makeRouteTrie().compress(); //System.out.println(trie); RouteMatch routeMatch = trie.find(OPTIONS, new URIMatch("/this/isno")); assertThat(routeMatch.routes, is(notNullValue())); assertThat(routeMatch.routes.keySet(), containsInAnyOrder(GET, POST, PUT, DELETE)); assertThat(routeMatch.route, is(nullValue())); routeMatch = trie.find(OPTIONS, new URIMatch("/this/is/the/best")); assertThat(routeMatch.routes, is(notNullValue())); assertThat(routeMatch.routes.keySet(), contains(GET)); assertThat(routeMatch.route, is(nullValue())); routeMatch = trie.find(HEAD, new URIMatch("/this/isno")); assertThat(routeMatch.routes, is(notNullValue())); assertThat(routeMatch.route.method(), is(GET)); assertThat(routeMatch.route.mapping(), is(result(-300))); } @Test public void testDuplicateRoute() { RouteTrie trie = new RouteTrie() .addRoute(new Route(POST, "/this/is", STATIC, "/success")); try { trie.addRoute(new Route(POST, "/this/is", STATIC, "/failure")); fail(); } catch (IllegalArgumentException iae) { assertThat( iae.getMessage(), is("duplicate route POST /this/is to static mapped as '/failure' with params null, current config = {POST=route POST /this/is to static mapped as '/success' with params null}") ); } } }