Skip to content

Commit 48d88db

Browse files
authored
Feature/routing separate from plugin (#68)
* Refactor plugin handling by introducing `Router` abstraction; added `SimpleRouter` implementation. Updated pipeline and tests to support new routing system. * Refactor plugin handling by introducing `Router` abstraction; added `SimpleRouter` implementation. Updated pipeline and tests to support new routing system. * Improve wildcard routing in `SimpleRouter` with longest-prefix match logic; add test coverage for specific and wildcard match scenarios.
1 parent b716a70 commit 48d88db

11 files changed

Lines changed: 287 additions & 33 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
<plugin>
123123
<groupId>com.diffplug.spotless</groupId>
124124
<artifactId>spotless-maven-plugin</artifactId>
125-
<version>2.43.0</version>
125+
<version>3.2.1</version>
126126
<configuration>
127127
<java>
128128
<includes>

src/main/java/org/juv25d/App.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import org.juv25d.filter.LoggingFilter;
55
import org.juv25d.logging.ServerLogging;
66
import org.juv25d.http.HttpParser;
7+
import org.juv25d.plugin.NotFoundPlugin; // New import
78
import org.juv25d.plugin.StaticFilesPlugin;
9+
import org.juv25d.router.SimpleRouter; // New import
810
import org.juv25d.util.ConfigLoader;
911

1012
import java.util.Set;
@@ -25,7 +27,14 @@ public static void main(String[] args) {
2527
Set.of()
2628
), 0);
2729
pipeline.addGlobalFilter(new LoggingFilter(), 0);
28-
pipeline.setPlugin(new StaticFilesPlugin());
30+
31+
// Initialize and configure SimpleRouter
32+
SimpleRouter router = new SimpleRouter();
33+
router.registerPlugin("/", new StaticFilesPlugin()); // Register StaticFilesPlugin for the root path
34+
router.registerPlugin("/*", new StaticFilesPlugin()); // Register StaticFilesPlugin for all paths
35+
router.registerPlugin("/notfound", new NotFoundPlugin()); // Example: Register NotFoundPlugin for a specific path
36+
37+
pipeline.setRouter(router); // Set the router in the pipeline
2938

3039
DefaultConnectionHandlerFactory handlerFactory =
3140
new DefaultConnectionHandlerFactory(httpParser, logger, pipeline);

src/main/java/org/juv25d/Pipeline.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import org.juv25d.filter.Filter;
44
import org.juv25d.filter.FilterChainImpl;
55
import org.juv25d.http.HttpRequest;
6-
import org.juv25d.plugin.Plugin;
6+
import org.juv25d.router.Router; // New import
77

88
import java.util.ArrayList;
99
import java.util.Collections;
@@ -18,7 +18,7 @@ public class Pipeline {
1818
private final List<FilterRegistration> globalFilters = new CopyOnWriteArrayList<>();
1919
private final Map<String, List<FilterRegistration>> routeFilters = new ConcurrentHashMap<>();
2020
private volatile List<Filter> sortedGlobalFilters = List.of();
21-
private Plugin plugin;
21+
private volatile Router router; // Changed from Plugin plugin;
2222

2323
public void addGlobalFilter(Filter filter, int order) {
2424
globalFilters.add(new FilterRegistration(filter, order, null));
@@ -33,11 +33,11 @@ public void addRouteFilter(Filter filter, int order, String pattern) {
3333
.add(new FilterRegistration(filter, order, pattern));
3434
}
3535

36-
public void setPlugin(Plugin plugin) {
37-
if (plugin == null) {
38-
throw new IllegalArgumentException("Plugin cannot be null");
36+
public void setRouter(Router router) {
37+
if (router == null) {
38+
throw new IllegalArgumentException("Router cannot be null");
3939
}
40-
this.plugin = plugin;
40+
this.router = router;
4141
}
4242

4343
public FilterChainImpl createChain(HttpRequest request) {
@@ -61,14 +61,14 @@ public FilterChainImpl createChain(HttpRequest request) {
6161
.forEach(filters::add);
6262
}
6363
}
64-
return new FilterChainImpl(filters, plugin);
64+
return new FilterChainImpl(filters, router); // Pass router instead of plugin
6565
}
6666

6767
public List<Filter> getFilters() {
6868
return Collections.unmodifiableList(sortedGlobalFilters);
6969
}
7070

71-
public Plugin getPlugin() {
72-
return plugin;
71+
public Router getRouter() { // Renamed from getPlugin
72+
return router;
7373
}
7474
}

src/main/java/org/juv25d/filter/FilterChainImpl.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
import org.juv25d.http.HttpRequest;
44
import org.juv25d.http.HttpResponse;
5-
import org.juv25d.plugin.Plugin;
5+
import org.juv25d.router.Router; // New import
66

77
import java.io.IOException;
88
import java.util.List;
99

1010
public class FilterChainImpl implements FilterChain {
1111

1212
private final List<Filter> filters;
13-
private final Plugin plugin;
13+
private final Router router; // Changed from Plugin plugin;
1414
private int index = 0;
1515

16-
public FilterChainImpl(List<Filter> filters, Plugin plugin) {
16+
public FilterChainImpl(List<Filter> filters, Router router) { // Changed constructor parameter
1717
this.filters = filters;
18-
this.plugin = plugin;
18+
this.router = router; // Changed assignment
1919
}
2020

2121
@Override
@@ -24,9 +24,7 @@ public void doFilter(HttpRequest req, HttpResponse res) throws IOException {
2424
Filter next = filters.get(index++);
2525
next.doFilter(req, res, this);
2626
} else {
27-
if (plugin != null) {
28-
plugin.handle(req, res);
29-
}
27+
router.resolve(req).handle(req, res); // Use router to resolve and handle
3028
}
3129
}
3230
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.juv25d.router;
2+
3+
import org.juv25d.http.HttpRequest;
4+
import org.juv25d.plugin.Plugin;
5+
6+
/**
7+
* The Router interface defines a contract for components that resolve an incoming HTTP request
8+
* to a specific Plugin instance responsible for handling that request.
9+
*/
10+
public interface Router {
11+
12+
/**
13+
* Resolves the given HttpRequest to a Plugin that can handle it.
14+
*
15+
* @param request The incoming HttpRequest.
16+
* @return The Plugin instance responsible for handling the request. Must not be null.
17+
*/
18+
Plugin resolve(HttpRequest request);
19+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.juv25d.router;
2+
3+
import org.juv25d.http.HttpRequest;
4+
import org.juv25d.plugin.NotFoundPlugin;
5+
import org.juv25d.plugin.Plugin;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.Comparator;
10+
11+
/**
12+
* A simple router implementation that maps request paths to specific Plugin instances.
13+
* If no specific plugin is registered for a path, it defaults to a NotFoundPlugin.
14+
*/
15+
public class SimpleRouter implements Router {
16+
17+
private final Map<String, Plugin> routes;
18+
private final Plugin notFoundPlugin;
19+
20+
public SimpleRouter() {
21+
this.routes = new HashMap<>();
22+
this.notFoundPlugin = new NotFoundPlugin();
23+
}
24+
25+
/**
26+
* Registers a plugin for a specific path.
27+
*
28+
* @param path The path for which the plugin should handle requests.
29+
* @param plugin The plugin to handle requests for the given path.
30+
*/
31+
public void registerPlugin(String path, Plugin plugin) {
32+
routes.put(path, plugin);
33+
}
34+
35+
/**
36+
* Resolves the given HttpRequest to a Plugin that can handle it.
37+
* Resolution order:
38+
* 1. Exact path match
39+
* 2. Wildcard match (longest prefix wins)
40+
* 3. NotFoundPlugin
41+
*
42+
* @param request The incoming HttpRequest.
43+
* @return The Plugin instance responsible for handling the request.
44+
*/
45+
@Override
46+
public Plugin resolve(HttpRequest request) {
47+
String path = request.path();
48+
49+
// 1. Exact match
50+
Plugin exactMatch = routes.get(path);
51+
if (exactMatch != null) {
52+
return exactMatch;
53+
}
54+
55+
// 2. Wildcard match (deterministic: longest prefix first)
56+
return routes.entrySet().stream()
57+
.filter(entry -> entry.getKey().endsWith("/*"))
58+
.sorted(Comparator.comparingInt(
59+
entry -> -entry.getKey().length()
60+
)) // longest (most specific) first
61+
.filter(entry -> {
62+
String prefix = entry.getKey().substring(0, entry.getKey().length() - 1);
63+
return path.startsWith(prefix);
64+
})
65+
.map(Map.Entry::getValue)
66+
.findFirst()
67+
.orElse(notFoundPlugin);
68+
}
69+
}

src/test/java/org/juv25d/PipelineTest.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@
22

33
import org.junit.jupiter.api.Test;
44
import org.juv25d.plugin.HelloPlugin;
5+
import org.juv25d.router.SimpleRouter; // New import
56

67
import static org.junit.jupiter.api.Assertions.*;
78

89
class PipelineTest {
910

1011
@Test
11-
void throwsExceptionWhenSettingNullPlugin() {
12+
void throwsExceptionWhenSettingNullRouter() { // Renamed test method
1213
Pipeline pipeline = new Pipeline();
13-
assertThrows(IllegalArgumentException.class, () -> pipeline.setPlugin(null));
14+
assertThrows(IllegalArgumentException.class, () -> pipeline.setRouter(null)); // Changed to setRouter
1415
}
1516

1617
@Test
17-
void customPluginIsUsed() {
18+
void customRouterIsUsed() { // Renamed test method
1819
Pipeline pipeline = new Pipeline();
20+
SimpleRouter router = new SimpleRouter(); // Use SimpleRouter
1921
HelloPlugin hello = new HelloPlugin();
22+
router.registerPlugin("/hello", hello); // Register a plugin
2023

21-
pipeline.setPlugin(hello);
24+
pipeline.setRouter(router); // Changed to setRouter
2225

23-
assertEquals(hello, pipeline.getPlugin());
26+
assertEquals(router, pipeline.getRouter()); // Changed to getRouter
2427
}
2528
}

src/test/java/org/juv25d/filter/FilterChainImplTest.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.juv25d.http.HttpRequest;
44
import org.juv25d.http.HttpResponse;
55
import org.juv25d.plugin.Plugin;
6+
import org.juv25d.router.SimpleRouter; // New import
67
import org.junit.jupiter.api.Test;
78

89
import java.io.IOException;
@@ -33,20 +34,22 @@ void filters_areCalledInOrderAndPluginLast() throws IOException {
3334
};
3435

3536
Plugin plugin = (req, res) -> calls.add("plugin");
37+
SimpleRouter router = new SimpleRouter();
38+
router.registerPlugin("/", plugin); // Register the plugin with a path
3639

3740
FilterChainImpl chain = new FilterChainImpl(
3841
List.of(f1, f2),
39-
plugin
42+
router // Pass the router
4043
);
4144

4245
HttpRequest req = new HttpRequest(
4346
"GET",
4447
"/",
45-
null,
48+
null, // queryString
4649
"HTTP/1.1",
4750
Map.of(),
4851
new byte[0],
49-
"UNKNOWN"
52+
"UNKNOWN" // remoteIp
5053
);
5154

5255
chain.doFilter(req, new HttpResponse(200, "OK", new HashMap<>(), new byte[0]));
@@ -73,14 +76,22 @@ void filter_canStopChainExecution() throws IOException {
7376
};
7477

7578
Plugin plugin = (req, res) -> calls.add("plugin");
79+
SimpleRouter router = new SimpleRouter();
80+
router.registerPlugin("/", plugin); // Register the plugin with a path
7681

7782
FilterChainImpl chain = new FilterChainImpl(
7883
List.of(blockingFilter),
79-
plugin
84+
router // Pass the router
8085
);
8186

8287
HttpRequest req = new HttpRequest(
83-
"GET", "/", null, "HTTP/1.1", Map.of(), new byte[0], "UNKNOWN"
88+
"GET",
89+
"/",
90+
null, // queryString
91+
"HTTP/1.1",
92+
Map.of(),
93+
new byte[0],
94+
"UNKNOWN" // remoteIp
8495
);
8596

8697
chain.doFilter(req, new HttpResponse(200, "OK", new HashMap<>(), new byte[0]));
@@ -94,14 +105,22 @@ void plugin_isCalledWhenNoFiltersExist() throws IOException {
94105
List<String> calls = new ArrayList<>();
95106

96107
Plugin plugin = (req, res) -> calls.add("plugin");
108+
SimpleRouter router = new SimpleRouter();
109+
router.registerPlugin("/", plugin); // Register the plugin with a path
97110

98111
FilterChainImpl chain = new FilterChainImpl(
99112
List.of(),
100-
plugin
113+
router // Pass the router
101114
);
102115

103116
HttpRequest req = new HttpRequest(
104-
"GET", "/", null, "HTTP/1.1", Map.of(), new byte[0], "UNKNOWN"
117+
"GET",
118+
"/",
119+
null, // queryString
120+
"HTTP/1.1",
121+
Map.of(),
122+
new byte[0],
123+
"UNKNOWN" // remoteIp
105124
);
106125

107126
chain.doFilter(req, new HttpResponse(200, "OK", new HashMap<>(), new byte[0]));

src/test/java/org/juv25d/filter/GlobalFilterTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.juv25d.http.HttpResponse;
66
import org.juv25d.plugin.Plugin;
77
import org.juv25d.Pipeline;
8+
import org.juv25d.router.SimpleRouter; // New import
89

910
import java.io.IOException;
1011

@@ -19,7 +20,11 @@ void globalFilter_shouldExecute_forAnyRoute() throws Exception {
1920

2021
RecordingFilter global = new RecordingFilter("global");
2122
pipeline.addGlobalFilter(global, 1);
22-
pipeline.setPlugin(new NoOpPlugin());
23+
24+
// Configure SimpleRouter and set it in the pipeline
25+
SimpleRouter router = new SimpleRouter();
26+
router.registerPlugin("/", new NoOpPlugin()); // Register NoOpPlugin for the root path
27+
pipeline.setRouter(router); // Set the router in the pipeline
2328

2429
execute(pipeline, "/anything");
2530

src/test/java/org/juv25d/filter/RouteFilterTests.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.juv25d.http.HttpRequest;
66
import org.juv25d.http.HttpResponse;
77
import org.juv25d.plugin.Plugin;
8+
import org.juv25d.router.SimpleRouter; // New import
89

910
import java.io.IOException;
1011

@@ -19,7 +20,12 @@ void routeFilter_shouldOnlyRun_whenRouteMatches() throws Exception {
1920

2021
RecordingFilter route = new RecordingFilter("route");
2122
pipeline.addRouteFilter(route, 1, "/api/*");
22-
pipeline.setPlugin(new NoOpPlugin());
23+
24+
// Configure SimpleRouter and set it in the pipeline
25+
SimpleRouter router = new SimpleRouter();
26+
router.registerPlugin("/", new NoOpPlugin()); // Register NoOpPlugin for the root path
27+
pipeline.setRouter(router); // Set the router in the pipeline
28+
2329
execute(pipeline, "/api/test");
2430
assertTrue(route.wasExecuted());
2531
route.reset();
@@ -30,7 +36,12 @@ void routeFilter_shouldOnlyRun_whenRouteMatches() throws Exception {
3036
@Test
3137
void routeFilter_shouldMatchExactPath() throws Exception {
3238
Pipeline pipeline = new Pipeline();
33-
pipeline.setPlugin(new NoOpPlugin());
39+
40+
// Configure SimpleRouter and set it in the pipeline
41+
SimpleRouter router = new SimpleRouter();
42+
router.registerPlugin("/", new NoOpPlugin()); // Register NoOpPlugin for the root path
43+
pipeline.setRouter(router); // Set the router in the pipeline
44+
3445
RecordingFilter exact = new RecordingFilter("exact");
3546
pipeline.addRouteFilter(exact, 1, "/admin");
3647
execute(pipeline, "/admin");

0 commit comments

Comments
 (0)