( Note: You can read the full series at: https://blog.mandraketech.in/series/java-http-server )
As I started working on the Static file server piece of the http server project, I realised that I did not have enough test cases for the Router implemented in the last article.
So, I started adding more tests. Specifically around the "longest prefix". As I did that, the following cases started failing:
Route added for
/ping
, and a request for/pingping
is receivedRoute added for
/api
, and request received for/api/user
So, once the tests were added and failing, there was scope to refactor the router lookup code to do better. I asked Cody AI: "Can this be done better ?", and it suggested that I use a TreeMap
and a floorKey
to get the longest key match. That was a good hint for me to pick up the rest of the pieces. Also, while I was there, I saw that there were a few interfaces, that could be replaced with equivalents from the java.util.function
package. This allowed the Router to be a little more "functional" in its style.
But first, time to get rid of the RouteBuilder
class. The createRouter
was replaced with the following:
public static final Router createRouter(Consumer<RouteManager> routeConfig) {
var routes = new TreeMap<String, Function<RouteRequest, RouteResponse>>();
routeConfig.accept(new RouteManager() {
@Override
public RouteManager addHandler(String path, Function<RouteRequest, RouteResponse> handler) {
if (routes.containsKey(path)) {
throw new IllegalArgumentException("Endpoint already registered: %s".formatted(path));
}
routes.put(path, handler);
return this;
}
});
return new Router(routes);
}
Here are the key changes from the previous implementation:
The
RouteManager
interface is gone. So no more dependencies for that in the caller of theaddHandler
More importantly,
addHandler
is now a fluent style api. It returns a reference to itself, so the caller code can be written with fewer breaks. So the router initializer can be written such.var rootRouter = Router.createRouter((r) -> { r.addHandler("/ping", new PingHandler()::handle) .addHandler("/health", new HealthHandler()::handle); });
The functionality for finding the relevant handler has been fixed to take care of:
"leaf" endpoints registered with a handler
"node" endpoints registered with a handler
private Function<RouteRequest, RouteResponse> findHandlerForReq(String path) {
Function<RouteRequest, RouteResponse> handler = null;
String routeKey = routes.floorKey(path);
if (routeKey == null) {
return null;
}
if (RouteHelpers.isRoutePrefx(routeKey, path) || path.equals(routeKey)) {
handler = routes.get(routeKey);
}
return handler;
}
static boolean isRoutePrefx(String prefixKey, String path) {
return prefixKey.endsWith("/") && path.startsWith(prefixKey);
}
The test cases are now "fixed". The importance of designing test cases with completeness. And I have been negligent. But, that is fine. This is a learning project, and I am using my assistants ChatGPT, and Cody AI to give me the tests. And sometimes they will miss things. This time, when I asked them for additional rest cases, they covered the nested cases better.
Also, in this process, the Router has now been made independent of the HttpHandler
's handle
function. handle
looks like this now:
public void handle(HttpExchange exchange) throws IOException {
RouteRequest req = RouteHelpers.createRouteRequest(exchange);
var response = processRequest(req);
// send the response, and clean up
sendResponse(exchange, response);
}
So, all the testing can happen against the processRequest
method.
There is also use of a new pattern in this code, for defining helper, or utility, functions that are private to this class. I have used an interface RouteHelpers
which has static implementations of functions that do not need any state from the router. This keeps my Router
clear of any private methods that do not need to be there. My attempt at being more functional. Don't know if this is a good pattern or not, but certainly helps when I ask Cody AI to help with writing tests. Less for the assistant to process.
The RouterTest
also uses a similar pattern to create a functional approach. With the possibility of making the Helper
a more widely used interface at a later point, if needed.
The Router
's handler is now a Function<RouteRequest, RouteResponse>
type. I can already see that there will be challenges in this function signature, when using chaining handlers. But, I am going to leave that until later. Fight it when we get there, and, more importantly, have a use case for it. My guess is that the functional style of programming may allow us to compose chains of functionality easily. Until then, this is a one works.
Until next time.
The Merge request for the tests added, and the code changes can be found here on the Gitlab Project.