From 4f98b7785c66fe2c97f61b818883d27274d993c1 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 09:30:01 -0500 Subject: [PATCH 1/6] Configurable CacheFilter Fixes #58 --- .../presentation/filter/CacheFilter.java | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java index 503d1f6a..e9c4d665 100644 --- a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java +++ b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java @@ -14,7 +14,10 @@ import javax.servlet.http.HttpServletResponseWrapper; /** - * WebFilter for setting response cache directives. + * WebFilter for setting response cache directives. By default, will set maximum cache (1 year) for + * all responses with a mime type in the CACHEABLE_CONTENT_TYPES array. In a given servlet (request + * handler) the CACHEABLE_RESPONSE attribute can be used to override the default behavior and either + * forcibly set to OFF (no cache headers set) or MAX (set max cache headers of 1 year). * * @author ryans */ @@ -27,6 +30,17 @@ public class CacheFilter implements Filter { private static final long EXPIRE_MILLIS = 31536000000L; // 365 days is max expires per spec + // Name of attribute to set on request processor to override default CachableResponse Auto + // behavior. + // Value should be CachableResponse enum + public final String CACHEABLE_RESPONSE = "CACHEABLE_RESPONSE"; + + public enum CachableResponse { + MAX, // Cache for spec max of 1 year + OFF, // Don't cache in filter, so maybe you can do your own cache logic + AUTO // Based on content type, will determine either OFF or MAX. This is default. + } + private static final String[] CACHEABLE_CONTENT_TYPES = new String[] { "text/css", @@ -51,7 +65,9 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; - chain.doFilter(request, new CacheControlResponse(httpResponse)); + CachableResponse cachable = (CachableResponse) request.getAttribute(CACHEABLE_RESPONSE); + + chain.doFilter(request, new CacheControlResponse(httpResponse, cachable)); } @Override @@ -62,25 +78,43 @@ public void destroy() {} class CacheControlResponse extends HttpServletResponseWrapper { - CacheControlResponse(HttpServletResponse response) { + private CachableResponse cachable; + + CacheControlResponse(HttpServletResponse response, CachableResponse cachable) { super(response); + this.cachable = cachable; } @Override public void setContentType(String type) { super.setContentType(type); - if (type != null && Arrays.binarySearch(CACHEABLE_CONTENT_TYPES, type) > -1) { - super.setDateHeader("Expires", System.currentTimeMillis() + EXPIRE_MILLIS); - super.setHeader( - "Cache-Control", null); // Remove header automatically added by SSL/TLS container module - super.setHeader( - "Pragma", null); // Remove header automatically added by SSL/TLS container module - } else { - super.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // HTTP 1.1 - super.setHeader("Pragma", "no-cache"); // HTTP 1.0 - super.setDateHeader("Expires", 0); // Proxies + if (cachable == null || cachable == CachableResponse.AUTO) { + + if (type != null && Arrays.binarySearch(CACHEABLE_CONTENT_TYPES, type) > -1) { + setMaxCache(); + } else { + setNoCache(); + } + } else if (cachable == CachableResponse.MAX) { + setMaxCache(); + } else { // OFF + setNoCache(); } } + + private void setNoCache() { + super.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // HTTP 1.1 + super.setHeader("Pragma", "no-cache"); // HTTP 1.0 + super.setDateHeader("Expires", 0); // Proxies + } + + private void setMaxCache() { + super.setDateHeader("Expires", System.currentTimeMillis() + EXPIRE_MILLIS); + super.setHeader( + "Cache-Control", null); // Remove header automatically added by SSL/TLS container module + super.setHeader( + "Pragma", null); // Remove header automatically added by SSL/TLS container module + } } } From 060653e2c85c6f54c7ec7b5d3efed6dc66430a28 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 09:44:59 -0500 Subject: [PATCH 2/6] Configurable CacheFilter --- .../org/jlab/smoothness/presentation/filter/CacheFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java index e9c4d665..76cae925 100644 --- a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java +++ b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java @@ -28,7 +28,7 @@ asyncSupported = true) public class CacheFilter implements Filter { - private static final long EXPIRE_MILLIS = 31536000000L; // 365 days is max expires per spec + public static final long MAX_EXPIRE_MILLIS = 31536000000L; // 365 days is max expires per spec // Name of attribute to set on request processor to override default CachableResponse Auto // behavior. @@ -110,7 +110,7 @@ private void setNoCache() { } private void setMaxCache() { - super.setDateHeader("Expires", System.currentTimeMillis() + EXPIRE_MILLIS); + super.setDateHeader("Expires", System.currentTimeMillis() + MAX_EXPIRE_MILLIS); super.setHeader( "Cache-Control", null); // Remove header automatically added by SSL/TLS container module super.setHeader( From 9afb5fc20c672456388e1522f3b60c9d08efc0a4 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 10:26:40 -0500 Subject: [PATCH 3/6] Configurable CacheFilter --- .../presentation/filter/CacheFilter.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java index 76cae925..3c99b378 100644 --- a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java +++ b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java @@ -16,7 +16,7 @@ /** * WebFilter for setting response cache directives. By default, will set maximum cache (1 year) for * all responses with a mime type in the CACHEABLE_CONTENT_TYPES array. In a given servlet (request - * handler) the CACHEABLE_RESPONSE attribute can be used to override the default behavior and either + * handler) cast response to CacheControlResponse to override the default behavior and either * forcibly set to OFF (no cache headers set) or MAX (set max cache headers of 1 year). * * @author ryans @@ -30,11 +30,6 @@ public class CacheFilter implements Filter { public static final long MAX_EXPIRE_MILLIS = 31536000000L; // 365 days is max expires per spec - // Name of attribute to set on request processor to override default CachableResponse Auto - // behavior. - // Value should be CachableResponse enum - public final String CACHEABLE_RESPONSE = "CACHEABLE_RESPONSE"; - public enum CachableResponse { MAX, // Cache for spec max of 1 year OFF, // Don't cache in filter, so maybe you can do your own cache logic @@ -65,9 +60,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; - CachableResponse cachable = (CachableResponse) request.getAttribute(CACHEABLE_RESPONSE); - - chain.doFilter(request, new CacheControlResponse(httpResponse, cachable)); + chain.doFilter(request, new CacheControlResponse(httpResponse)); } @Override @@ -78,11 +71,15 @@ public void destroy() {} class CacheControlResponse extends HttpServletResponseWrapper { - private CachableResponse cachable; + private CachableResponse cachable = CachableResponse.AUTO; - CacheControlResponse(HttpServletResponse response, CachableResponse cachable) { + CacheControlResponse(HttpServletResponse response) { super(response); + } + + public void setContentType(String type, CachableResponse cachable) { this.cachable = cachable; + this.setContentType(type); } @Override From 6a223e487539f6a3a6caa003490a301c06d29333 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 10:34:39 -0500 Subject: [PATCH 4/6] Configurable CacheFilter --- .../presentation/filter/CacheFilter.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java index 3c99b378..6983d9ac 100644 --- a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java +++ b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java @@ -36,6 +36,12 @@ public enum CachableResponse { AUTO // Based on content type, will determine either OFF or MAX. This is default. } + /** + * Only mime types of files that ALWAYS should be cached are included. Notably excluded are types + * text/html and application/json. These rarely should be cached, though sometimes they should and + * in those cases the Servlet needs to manually cast response to CacheControlResponse and use + * setContentType(type, cachable) with value CachableResponse.MAX. + */ private static final String[] CACHEABLE_CONTENT_TYPES = new String[] { "text/css", @@ -69,6 +75,14 @@ public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} + /** + * We use a Wrapper because some application servers don't allow you to set Headers in + * post-processing (filter chain), and in pre-processing the content type often isn't known yet. + * Wrapper allows us to run filter code during servlet processing at the specific moment we learn + * of content type. + * + *

https://stackoverflow.com/questions/2563344/how-to-add-response-headers-based-on-content-type-getting-content-type-before-t + */ class CacheControlResponse extends HttpServletResponseWrapper { private CachableResponse cachable = CachableResponse.AUTO; From 4a6135351a94be36a67c3b2df2e49549aa25ec06 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 11:47:57 -0500 Subject: [PATCH 5/6] Configurable CacheFilter --- .../demo/presentation/controller/Test.java | 37 +++++++++++++++++++ .../presentation/filter/CacheFilter.java | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java diff --git a/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java b/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java new file mode 100644 index 00000000..81ec25eb --- /dev/null +++ b/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java @@ -0,0 +1,37 @@ +package org.jlab.demo.presentation.controller; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.jlab.smoothness.presentation.filter.CacheFilter; + +/** + * @author ryans + */ +@WebServlet( + name = "test", + urlPatterns = {"/test"}) +public class Test extends HttpServlet { + + /** + * Handles the HTTP GET method. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // Let's test forcibly caching response! + CacheFilter.CacheControlResponse cachableResponse= (CacheFilter.CacheControlResponse)response; + cachableResponse.setContentType("application/json", CacheFilter.CachableResponse.MAX); + + response.getWriter().println("{\"name\":\"test\"}"); + } +} diff --git a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java index dcb8d31f..04ab4ae8 100644 --- a/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java +++ b/smoothness-weblib/src/main/java/org/jlab/smoothness/presentation/filter/CacheFilter.java @@ -83,7 +83,7 @@ public void destroy() {} * *

https://stackoverflow.com/questions/2563344/how-to-add-response-headers-based-on-content-type-getting-content-type-before-t */ - class CacheControlResponse extends HttpServletResponseWrapper { + public static class CacheControlResponse extends HttpServletResponseWrapper { private CachableResponse cachable = CachableResponse.AUTO; From 1d01a8135070e40ee0982b085a82660fa0119611 Mon Sep 17 00:00:00 2001 From: ryans Date: Thu, 22 Jan 2026 11:50:09 -0500 Subject: [PATCH 6/6] Configurable CacheFilter --- .../main/java/org/jlab/demo/presentation/controller/Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java b/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java index 81ec25eb..f99c0fe1 100644 --- a/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java +++ b/smoothness-demo/src/main/java/org/jlab/demo/presentation/controller/Test.java @@ -29,7 +29,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Let's test forcibly caching response! - CacheFilter.CacheControlResponse cachableResponse= (CacheFilter.CacheControlResponse)response; + CacheFilter.CacheControlResponse cachableResponse = (CacheFilter.CacheControlResponse) response; cachableResponse.setContentType("application/json", CacheFilter.CachableResponse.MAX); response.getWriter().println("{\"name\":\"test\"}");