--- sessionId: session-260923-111902-5uwc --- # Requirements ### Overview & Goals Replace the hand-rolled `com.sun.net.httpserver` stack in `me.bobert.netneutrality.http.server` with Javalin, so the bot's embedded API needs far fewer custom classes and gains correct status codes, JSON handling and error responses. Additionally simplify the outbound `WebModule` HTTP client. ### Scope **In scope** - `WebApi` becomes a thin Javalin-based `Lifecycle` component (same priority 70, same `NETNEUTRALITY_API_PORT` setting). - Delete `AbstractHandler`, `RootHandler`, `ActionHandler`, `GuildHandler`; their logic moves into small route-registration units. - Standardise the API layer on **Gson** — remove `org.json` (`JSONObject`/`JSONArray`) usage from the HTTP server package. - Correct HTTP semantics: 404 for unknown guilds/paths, 405 for wrong methods, 500 on exceptions, UTF-8 safe bodies. - Simplify `WebModule` (outbound OkHttp wrapper): extract the static cache, remove duplicated/unused members. **Out of scope** - Changing the public URL shape of existing endpoints (`/`, `/actions/*`, `/guilds/*`) — paths and payload keys stay backwards compatible. - Authentication/authorisation for the API. - Migrating `org.json` usage elsewhere in the project (e.g. task classes using `WebModule.getAsObject()`). ### Functional Requirements - `GET /` → HTML liveness page (unchanged text). - `GET /actions/status` → `{"health":...,"ping":...}`. - `POST /actions/shutdown` → runs the shutdown hook and exits (accept `GET` too for compatibility if desired). - `GET /actions/settings/current/{guildId}` → `{"status":"ok","settings":{...}}`, or 404 + `{"status":"error"}` for an unknown guild. - `POST /actions/settings/reload/{guildId}` → reloads the guild setting cache, `{"status":"true"}`; 404 for an unknown guild. - `GET /guilds/owner` → array of `{guild_id, owner_id}`. - `GET /guilds/channels/{guildId}` and `GET /guilds/roles/{guildId}` → arrays of id/name objects; 404 for an unknown guild. - Requests arriving before JDA is ready must return 503 rather than throwing an NPE. ### Non-Functional Requirements - Startup/shutdown stays deterministic through `ApplicationContext` — `stop()` must release the port. - Shaded jar must still run (`maven-shade-plugin`); Javalin/Jetty service files need merging. - No behavioural regression for existing consumers of the endpoints. # Technical Design ### Current Implementation - `WebApi.java` (92 lines) implements `Lifecycle` (priority 70), creates an `HttpServer`, registers three contexts and a fixed pool built from `BaseThreadFactory("web-api")`. Constructed in `App.java:154`. - `AbstractHandler` offers `sendStringResponse`/`sendJsonResponse`; both hardcode status 200 and use `str.length()` instead of byte length (breaks non-ASCII). - `ActionHandler` / `GuildHandler` route via `if (path.startsWith(...))` chains and parse ids with `getRequestURI().toString().split("/")`; they mix Gson and `org.json`, never check the HTTP method, and call `jda.getGuildById()` twice in places. - `WebModule` is an unrelated *outbound* OkHttp wrapper with a builder, a static LRU cache and a `CachedResponse` record; it builds a brand-new `OkHttpClient` per request and has three redundant timeout setters plus unused `elapsedTime`/`errorMessage` accessors. ### Key Decisions 1. **Javalin** (6.x, JDK 17+ / Java 25 compatible) as the embedded server — removes routing, method matching, error mapping and content-type handling code. 2. **Gson only** in the HTTP layer: register a `GsonJsonMapper` on the Javalin config so `ctx.json(obj)` works with plain records/maps; `org.json` disappears from `http/server`. 3. **Route groups instead of handler classes**: `ActionRoutes` and `GuildRoutes` expose a single `static void register(Javalin app, deps...)`, so `AbstractHandler`, `RootHandler`, `ActionHandler`, `GuildHandler` are all deleted (4 classes → 2 tiny registrars). 4. **Guild lookup guard**: one private helper resolves `{guildId}` → `Guild` or throws `NotFoundResponse`, replacing repeated null checks. 5. **WebModule**: keep the builder API used by callers, but move the cache into a package-private `ResponseCache`, share a single static `OkHttpClient` and derive per-request timeouts with `newBuilder()`. ### Proposed Changes **`http/server/WebApi.java`** ```java public class WebApi implements Lifecycle { private Javalin app; @Override public int priority() { return 70; } @Override public void start() { int port = Integer.parseInt(settings.get("NETNEUTRALITY_API_PORT", "9000")); app = Javalin.create(cfg -> cfg.jsonMapper(new GsonJsonMapper())); app.get("/", ctx -> ctx.html(LIVENESS_HTML)); ActionRoutes.register(app, jdaSupplier, guildServices, shutdownHook); GuildRoutes.register(app, jdaSupplier); app.exception(Exception.class, (e, ctx) -> { log.error(...); ctx.status(500).json(Map.of("status","error")); }); app.start(port); } @Override public void stop() { if (app != null) { app.stop(); app = null; } } } ``` **`http/server/GuildRoutes.java`** — `/guilds/owner`, `/guilds/channels/{guildId}`, `/guilds/roles/{guildId}` mapped to `List>` and returned via `ctx.json(...)`. **`http/server/ActionRoutes.java`** — `/actions/status`, `/actions/shutdown`, `/actions/settings/current/{guildId}`, `/actions/settings/reload/{guildId}`. **`http/server/GsonJsonMapper.java`** — ~15 lines implementing Javalin's `JsonMapper` over a shared `Gson`. **`http/WebModule.java`** — shared client, cache extracted, redundant setters/accessors dropped; public builder methods used by callers remain. ### Data Models / Contracts Small records replace ad-hoc JSON building, serialized by Gson with the existing key names: ```java record GuildOwner(String guild_id, String owner_id) {} record NamedEntity(String channel_id, String channel_name) {} record StatusResponse(String health, String ping) {} ``` ### File Structure ``` src/main/java/me/bobert/netneutrality/http/ WebModule.java (modified) server/WebApi.java (rewritten, Javalin) server/GsonJsonMapper.java (new) server/ActionRoutes.java (new) server/GuildRoutes.java (new) server/AbstractHandler.java (deleted) server/RootHandler.java (deleted) server/ActionHandler.java (deleted) server/GuildHandler.java (deleted) pom.xml (+ io.javalin:javalin, shade transformer) ``` ### Architecture Diagram ```mermaid graph LR App-->ApplicationContext ApplicationContext-->WebApi WebApi-->Javalin WebApi-->GsonJsonMapper Javalin-->ActionRoutes Javalin-->GuildRoutes ActionRoutes-->GuildServiceProvider GuildRoutes-->JDA ``` ### Risks - **Shaded jar**: Jetty/Javalin rely on `META-INF/services`; the shade plugin needs `ServicesResourceTransformer`, otherwise startup fails only in the packaged build. - **Jar size / SLF4J binding**: Jetty logs through SLF4J; project already has log4j + slf4j — check for duplicate bindings. - **JDA not ready**: `jdaSupplier.get()` may be null; guard with a 503 mapping. - `System.exit(0)` in the shutdown route must run after the response is flushed. # Testing ### Validation Approach Build with `./mvnw -q verify` (checkstyle included), then start the app/`WebApi` on an ephemeral port and exercise every endpoint with `curl`, comparing payload shape against the pre-refactor behaviour. ### Key Scenarios - `GET /` returns 200 and the HTML liveness text. - `GET /actions/status` returns 200 `application/json` with `health` and `ping`. - `GET /guilds/owner` returns a JSON array with `guild_id`/`owner_id` keys. - `GET /guilds/channels/{id}` and `/guilds/roles/{id}` return arrays with `channel_id`/`channel_name`. - `GET /actions/settings/current/{id}` returns `{"status":"ok","settings":{...}}`. ### Edge Cases - Unknown guild id → 404 with `{"status":"error"}` (previously 200). - Unknown path → 404 (previously echoed the path with 200). - Wrong method on a route → 405. - Non-ASCII content in settings values round-trips correctly (old `str.length()` bug). - `Lifecycle.stop()` frees the port — a second `start()` on the same port succeeds. ### Test Changes - Add a lightweight JUnit test starting `WebApi` on port 0 with a mocked `Supplier` and `GuildServiceProvider`, hitting the routes with Javalin's test client or `java.net.http.HttpClient`. - Add a `WebModule` test covering cache hit/expiry after the cache extraction. # Delivery Steps ### ✓ Step 1: Add Javalin dependency and Gson JSON mapper The project builds with Javalin available and a Gson-backed JSON mapper ready for use. - Add `io.javalin:javalin` (6.x) to `pom.xml` dependencies. - Add `ServicesResourceTransformer` to the `maven-shade-plugin` configuration so Jetty/Javalin service files survive shading. - Create `http/server/GsonJsonMapper.java` implementing Javalin's `JsonMapper` over a shared `Gson` instance. - Verify `./mvnw -q package` succeeds and the shaded jar still starts. ### ✓ Step 2: Rewrite WebApi as a Javalin Lifecycle component `WebApi` starts and stops a Javalin server while keeping its Lifecycle contract and the root endpoint. - Replace the `HttpServer`/`ExecutorService` fields with a `Javalin app` field. - Build the instance in `start()` with the `GsonJsonMapper`, reading `NETNEUTRALITY_API_PORT` from `SettingsProvider` as today. - Register `GET /` returning the liveness HTML and delete `RootHandler.java`. - Add global `exception` and `error` handlers returning 500/404 JSON, plus a 503 guard when `jdaSupplier.get()` is null. - Implement `stop()` via `app.stop()`; keep `priority() == 70` and the `App.java` construction site unchanged. ### ✓ Step 3: Migrate guild endpoints to GuildRoutes All `/guilds/*` endpoints are served by Javalin route lambdas with proper status codes and Gson serialization. - Create `http/server/GuildRoutes.register(Javalin, Supplier)`. - Map `GET /guilds/owner`, `GET /guilds/channels/{guildId}`, `GET /guilds/roles/{guildId}` using path params instead of `split("/")`. - Return `List` payloads via `ctx.json(...)`, preserving the existing `guild_id`/`owner_id`/`channel_id`/`channel_name` keys. - Resolve guilds through a shared helper that throws `NotFoundResponse` for unknown ids (single `getGuildById` call). - Delete `GuildHandler.java`. ### ✓ Step 4: Migrate action endpoints to ActionRoutes All `/actions/*` endpoints run on Javalin, with `org.json` fully removed from the HTTP server package. - Create `http/server/ActionRoutes.register(Javalin, Supplier, GuildServiceProvider, Runnable)`. - Map `/actions/status`, `/actions/shutdown`, `/actions/settings/current/{guildId}`, `/actions/settings/reload/{guildId}`. - Replace `JSONObject`/`JSONArray` with Gson-serialized maps/records keeping the same response keys. - Ensure the shutdown route flushes its response before running the hook and exiting. - Delete `ActionHandler.java` and `AbstractHandler.java`, and add endpoint tests starting `WebApi` on port 0 with mocked JDA/guild services. ### ✓ Step 5: Simplify the outbound WebModule client `WebModule` keeps its builder API but sheds duplicated state and its inlined cache. - Extract the static LRU cache and `CachedResponse` into a package-private `ResponseCache` class in `http/`. - Share a single static `OkHttpClient` and derive per-request timeouts with `newBuilder()` instead of constructing a client per build. - Collapse the redundant `timeout`/`connectTimeout`/`readTimeout`/`writeTimeout` setters and remove unused accessors (`getErrorMessage`, `elapsedTime` exposure) after checking call sites. - Add a unit test covering cache hit, expiry and POST bypass; verify existing `WebModule` callers still compile.