From 9b716b8406726fea2b27509fdeec9f4fbe32091e Mon Sep 17 00:00:00 2001 From: Elias Haugsbakk Date: Mon, 27 Jul 2026 16:26:42 +0200 Subject: implement page caching --- .../java/no/eliashaugsbakk/clams/server/App.java | 67 ++++++++++++++++++++++ .../java/no/eliashaugsbakk/clams/server/Main.java | 65 --------------------- .../clams/server/config/AppContext.java | 4 ++ .../clams/server/config/AppRoutes.java | 53 +++++++++++++++-- .../clams/server/service/PageCache.java | 37 ++++++++++++ clams-server/src/main/resources/templates/404.html | 4 +- 6 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 clams-server/src/main/java/no/eliashaugsbakk/clams/server/App.java delete mode 100644 clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java create mode 100644 clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/PageCache.java (limited to 'clams-server/src') diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/App.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/App.java new file mode 100644 index 0000000..5910f43 --- /dev/null +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/App.java @@ -0,0 +1,67 @@ +package no.eliashaugsbakk.clams.server; + +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import io.javalin.Javalin; +import io.javalin.rendering.template.JavalinPebble; +import java.util.Map; +import no.eliashaugsbakk.clams.server.config.AppContext; +import no.eliashaugsbakk.clams.server.config.AppRoutes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class App { + private static final Logger log = LoggerFactory.getLogger(App.class); + void main() { + // Initialize dependencies + AppContext context = new AppContext(); + + try { + Javalin.create(config -> { + config.staticFiles.add(staticFiles -> { + staticFiles.hostedPath = "/"; + staticFiles.directory = "/public"; + }); + config.fileRenderer(new JavalinPebble()); + + config.routes.error(404, ctx -> { + if (ctx.path().startsWith("/api")) { + ctx.json(Map.of( + "error", "Not Found", + "message", "The requested API endpoint does not exist." + )); + } else { + ctx.render("templates/404.html", Map.of( + "page_title", "404 - Page Not Found", + "page_css", "404" + )); + } + }); + + config.routes.exception(UnrecognizedPropertyException.class, (e, ctx) -> ctx.status(400).json(Map.of( + "error", "Bad Request", + "message", "Unrecognized property: '" + e.getPropertyName() + "'" + ))); + + config.routes.exception(Exception.class, (e, ctx) -> { + log.error("Unhandled error on {} {}", ctx.method(), ctx.path(), e); + ctx.status(500); + + if (ctx.path().startsWith("/api")) { + ctx.json(Map.of("error", "Internal Server Error")); + } else { + ctx.result("Internal Server Error. Please try again later."); + } + }); + + config.routes.apiBuilder(new AppRoutes(context)); + config.events.serverStopped(context::close); + }).start(7070); + + + } catch (Exception e) { + context.close(); + log.error("Unexpected error", e); + throw e; + } + } +} diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java deleted file mode 100644 index eba65f8..0000000 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java +++ /dev/null @@ -1,65 +0,0 @@ -package no.eliashaugsbakk.clams.server; - -import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; -import io.javalin.Javalin; -import io.javalin.rendering.template.JavalinPebble; -import java.util.Map; -import no.eliashaugsbakk.clams.server.config.AppContext; -import no.eliashaugsbakk.clams.server.config.AppRoutes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Main { - private static final Logger log = LoggerFactory.getLogger(Main.class); - void main() { - // Initialize dependencies - AppContext context = new AppContext(); - - try { - Javalin.create(config -> { - config.staticFiles.add("/public"); - config.fileRenderer(new JavalinPebble()); - - config.routes.error(404, ctx -> { - if (ctx.path().startsWith("/api")) { - ctx.json(Map.of( - "error", "Not Found", - "message", "The requested API endpoint does not exist." - )); - } else { - ctx.render("templates/404.html", Map.of( - "page_title", "404 - Page Not Found", - "page_css", "404" - )); - } - }); - - config.routes.exception(UnrecognizedPropertyException.class, (e, ctx) -> ctx.status(400).json(Map.of( - "error", "Bad Request", - "message", "Unrecognized property: '" + e.getPropertyName() + "'" - ))); - - config.routes.exception(Exception.class, (e, ctx) -> { - log.error("Unhandled error on {} {}", ctx.method(), ctx.path(), e); - ctx.status(500); - - if (ctx.path().startsWith("/api")) { - ctx.json(Map.of("error", "Internal Server Error")); - } else { - ctx.result("Internal Server Error. Please try again later."); - } - }); - - config.routes.apiBuilder(new AppRoutes(context)); - - config.events.serverStopped(context::close); - }).start(7070); - - - } catch (Exception e) { - context.close(); - log.error("Unexpected error", e); - throw e; - } - } -} diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java index 2d52374..189edc7 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java @@ -13,6 +13,7 @@ import no.eliashaugsbakk.clams.server.repository.ProjectsRepo; import no.eliashaugsbakk.clams.server.repository.ProjectsRepoSqlite; import no.eliashaugsbakk.clams.server.repository.SqliteManager; import no.eliashaugsbakk.clams.server.service.AuthService; +import no.eliashaugsbakk.clams.server.service.PageCache; import no.eliashaugsbakk.clams.server.service.SlugService; public class AppContext implements AutoCloseable { @@ -22,6 +23,7 @@ public class AppContext implements AutoCloseable { private final PostController postController; private final ProjectsController projectsController; private final AuthService authService; + private final PageCache pageCache; public AppContext() { AppConfig appConfig = new AppConfig(); @@ -41,6 +43,7 @@ public class AppContext implements AutoCloseable { this.projectsController = new ProjectsController(projectsRepo); this.postController = new PostController(postsRepo, slugService); this.authService = new AuthService(appConfig); + this.pageCache = new PageCache(15); } public PostsController getPostsController() { return postsController; } @@ -48,6 +51,7 @@ public class AppContext implements AutoCloseable { public ProjectsController getProjectsController() { return projectsController; } public PostController getPostController() { return postController; } public AuthService getAuthService() { return authService; } + public PageCache pageCache() { return pageCache; } @Override public void close() { diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java index abd2926..b296f4f 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java @@ -1,5 +1,6 @@ package no.eliashaugsbakk.clams.server.config; +import static io.javalin.apibuilder.ApiBuilder.after; import static io.javalin.apibuilder.ApiBuilder.before; import static io.javalin.apibuilder.ApiBuilder.delete; import static io.javalin.apibuilder.ApiBuilder.get; @@ -19,6 +20,51 @@ public class AppRoutes implements EndpointGroup { @Override public void addEndpoints() { + before(ctx -> { + String path = ctx.path(); + + // Skip non-GET requests and API endpoints + if (!ctx.method().name().equalsIgnoreCase("GET") + || path.startsWith("/api") + || ctx.queryString() != null) { + return; + } + + String cachedHtml = appContext.pageCache().get(path); + if (cachedHtml != null) { + ctx.result(cachedHtml); + ctx.contentType("text/html"); + ctx.skipRemainingHandlers(); + } + }); + + after(ctx -> { + String method = ctx.method().name(); + String path = ctx.path(); + + // Invalidate cache on any modifying request if successful + if ((method.equalsIgnoreCase("POST") + || method.equalsIgnoreCase("PUT") + || method.equalsIgnoreCase("DELETE")) + && ctx.status().getCode() >= 200 && ctx.status().getCode() < 300) { + appContext.pageCache().clear(); + return; + } + + // 2. Cache GET responses + if (method.equalsIgnoreCase("GET") + && !path.startsWith("/api") // exclude API responses + && ctx.queryString() == null // exclude queried pages + && ctx.status().getCode() == 200) { // only cache successful responses + + String renderedHtml = ctx.result(); + if (renderedHtml != null && !renderedHtml.isBlank()) { + appContext.pageCache().put(path, renderedHtml); + } + } + }); + + get("/", ctx -> ctx.redirect("/home")); get("/home", ctx -> ctx.render("templates/home.html", Map.of("page_title", "Elias Haugsbakk", "page_css", "home"))); @@ -35,8 +81,8 @@ public class AppRoutes implements EndpointGroup { String authHeader = ctx.header("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { - ctx.status(401).json(Map.of("error", "Unauthorized", - "message", "Missing or malformed Authorization header.")); + ctx.status(401).json(Map.of("error", "Unauthorized", "message", + "Missing or malformed Authorization header.")); ctx.skipRemainingHandlers(); return; } @@ -44,8 +90,7 @@ public class AppRoutes implements EndpointGroup { String token = authHeader.substring(7).trim(); if (!appContext.getAuthService().isValid(token)) { ctx.status(403) - .json(Map.of("error", "Forbidden", - "message", "Invalid API validation token.")); + .json(Map.of("error", "Forbidden", "message", "Invalid API validation token.")); ctx.skipRemainingHandlers(); } }); diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/PageCache.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/PageCache.java new file mode 100644 index 0000000..4313cce --- /dev/null +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/PageCache.java @@ -0,0 +1,37 @@ +package no.eliashaugsbakk.clams.server.service; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public class PageCache { + private final Map cache; + + /** + * Least Recently Used Cache + */ + public PageCache(int capacity) { + + LinkedHashMap map = new LinkedHashMap<>(capacity, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } + }; + + // Wrap with synchronizedMap to make thread-safe + this.cache = Collections.synchronizedMap(map); + } + + public String get(String url) { + return cache.get(url); + } + + public void put(String url, String html) { + cache.put(url, html); + } + + public void clear() { + cache.clear(); + } +} diff --git a/clams-server/src/main/resources/templates/404.html b/clams-server/src/main/resources/templates/404.html index 3a655ae..0fa0329 100644 --- a/clams-server/src/main/resources/templates/404.html +++ b/clams-server/src/main/resources/templates/404.html @@ -4,10 +4,10 @@
404
-

Fil iIkke Funnet

+

Fil Ikke Funnet

Filen du ser etter finnes ikke.

-- cgit v1.2.3