/*
* 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}")
);
}
}
}