This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 61c8fa25c WW-5832 Bound the FreeMarker template cache (#2090)
61c8fa25c is described below

commit 61c8fa25cecdea0093a58a98c26730586a49281e
Author: g0w6y <[email protected]>
AuthorDate: Sat Nov 7 13:38:39 2026 +0530

    WW-5832 Bound the FreeMarker template cache (#2090)
    
    With struts.freemarker.mru.max.strong.size=0 (default) the FreeMarker 
template
    cache stayed on the default SoftCacheStorage, which has no entry limit. Its 
key
    includes the locale, which for LocaleProvider actions derives from the 
request,
    so distinct request locales grew it without a count bound.
    
    Use a SoftCacheStorage that is emptied once it holds 10000 entries. It stays
    concurrent, so lookups remain lock-free (unlike MruCacheStorage, which
    TemplateCache synchronizes on). The strong:N path is unchanged.
---
 .../views/freemarker/FreemarkerManager.java        | 25 ++++++++++++++++++++++
 .../views/freemarker/FreemarkerManagerTest.java    | 20 +++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git 
a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java 
b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
index 3824a43c6..057f0ff3f 100644
--- 
a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
+++ 
b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
@@ -28,6 +28,7 @@ import org.apache.struts2.util.ValueStack;
 import freemarker.cache.ClassTemplateLoader;
 import freemarker.cache.FileTemplateLoader;
 import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.SoftCacheStorage;
 import freemarker.cache.TemplateLoader;
 import freemarker.core.HTMLOutputFormat;
 import freemarker.core.TemplateClassResolver;
@@ -161,6 +162,7 @@ public class FreemarkerManager {
     private static final Logger LOG = 
LogManager.getLogger(FreemarkerManager.class);
     public static final String CONFIG_SERVLET_CONTEXT_KEY = 
"freemarker.Configuration";
     public static final String KEY_EXCEPTION = "exception";
+    static final int MAX_CACHED_TEMPLATES = 10000;
 
 
     protected String templatePath;
@@ -335,6 +337,9 @@ public class FreemarkerManager {
         if (mruMaxStrongSize > 0) {
             LOG.debug("Sets Configuration.CACHE_STORAGE_KEY to strong:{}", 
mruMaxStrongSize);
             configuration.setSetting(Configuration.CACHE_STORAGE_KEY, 
"strong:" + mruMaxStrongSize);
+        } else {
+            LOG.debug("Sets Configuration cache storage to a SoftCacheStorage 
bounded to {} entries", MAX_CACHED_TEMPLATES);
+            configuration.setCacheStorage(new 
BoundedSoftCacheStorage(MAX_CACHED_TEMPLATES));
         }
         if (templateUpdateDelay != null) {
             LOG.debug("Sets Configuration.TEMPLATE_UPDATE_DELAY_KEY to {}", 
templateUpdateDelay);
@@ -565,4 +570,24 @@ public class FreemarkerManager {
         }
     }
 
+    /**
+     * A {@link SoftCacheStorage} (concurrent, no locking on lookups) that is 
emptied once it holds {@code maxSize} entries; the bound is approximate under 
concurrency.
+     */
+    static class BoundedSoftCacheStorage extends SoftCacheStorage {
+
+        private final int maxSize;
+
+        BoundedSoftCacheStorage(int maxSize) {
+            this.maxSize = maxSize;
+        }
+
+        @Override
+        public void put(Object key, Object value) {
+            if (getSize() >= maxSize && get(key) == null) {
+                clear();
+            }
+            super.put(key, value);
+        }
+    }
+
 }
diff --git 
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
 
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
index 834983fca..cee2d9792 100644
--- 
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
+++ 
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
@@ -19,6 +19,8 @@
 package org.apache.struts2.views.freemarker;
 
 import org.apache.struts2.util.fs.DefaultFileManagerFactory;
+import freemarker.cache.CacheStorageWithGetSize;
+import freemarker.cache.ConcurrentCacheStorage;
 import freemarker.template.Configuration;
 import freemarker.template.Template;
 import freemarker.template.Version;
@@ -30,6 +32,7 @@ import jakarta.servlet.ServletContext;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 /**
@@ -138,6 +141,23 @@ public class FreemarkerManagerTest extends 
StrutsInternalTestCase {
         // then
         assertFalse(manager.config.getWhitespaceStripping());
     }
+
+    public void testTemplateCacheIsBoundedByDefault() throws Exception {
+        // given
+        FreemarkerManager manager = new FreemarkerManager();
+        container.inject(manager);
+        Configuration configuration = manager.getConfiguration(servletContext);
+
+        // when
+        for (int i = 0; i < FreemarkerManager.MAX_CACHED_TEMPLATES + 100; i++) 
{
+            
configuration.getTemplate("org/apache/struts2/views/freemarker/nested.ftl", new 
Locale("xx", "YY", "v" + i));
+        }
+
+        // then
+        CacheStorageWithGetSize storage = (CacheStorageWithGetSize) 
configuration.getCacheStorage();
+        assertTrue(storage.getSize() <= 
FreemarkerManager.MAX_CACHED_TEMPLATES);
+        assertTrue(((ConcurrentCacheStorage) storage).isConcurrent());
+    }
 }
 
 class DummyFreemarkerManager extends FreemarkerManager {

Reply via email to