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 963a81f43 WW-5647 Use ConcurrentHashMap for XSLT template cache (#1781)
963a81f43 is described below
commit 963a81f43df9b17a80f0892ff441d328f7979681
Author: Arun <[email protected]>
AuthorDate: Mon Jul 20 00:10:25 2026 +0530
WW-5647 Use ConcurrentHashMap for XSLT template cache (#1781)
* Use ConcurrentHashMap for XSLT template cache and add double-check locking
* Prevent noCache from polluting shared template cache; add dedup and
noCache regression tests
---
.../org/apache/struts2/result/xslt/XSLTResult.java | 38 +++---
.../apache/struts2/result/xslt/XSLTResultTest.java | 148 +++++++++++++++++++++
.../src/test/resources/XSLTResultTestDedup.xsl | 29 ++++
.../resources/XSLTResultTestNoCachePollution.xsl | 29 ++++
4 files changed, 226 insertions(+), 18 deletions(-)
diff --git
a/plugins/xslt/src/main/java/org/apache/struts2/result/xslt/XSLTResult.java
b/plugins/xslt/src/main/java/org/apache/struts2/result/xslt/XSLTResult.java
index b6d8ec62a..12f57ff34 100644
--- a/plugins/xslt/src/main/java/org/apache/struts2/result/xslt/XSLTResult.java
+++ b/plugins/xslt/src/main/java/org/apache/struts2/result/xslt/XSLTResult.java
@@ -45,7 +45,7 @@ import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
-import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
/**
@@ -68,11 +68,7 @@ public class XSLTResult implements Result {
/**
* Cache of all templates.
*/
- private static final Map<String, Templates> templatesCache;
-
- static {
- templatesCache = new HashMap<>();
- }
+ private static final Map<String, Templates> templatesCache = new
ConcurrentHashMap<>();
// Configurable Parameters
@@ -283,19 +279,25 @@ public class XSLTResult implements Result {
if (noCache || (templates == null)) {
synchronized (templatesCache) {
- URL resource =
ServletActionContext.getServletContext().getResource(path);
-
- if (resource == null) {
- throw new TransformerException("Stylesheet " + path + "
not found in resources.");
+ // Re-check after acquiring lock to avoid redundant compilation
+ templates = templatesCache.get(path);
+ if (noCache || (templates == null)) {
+ URL resource =
ServletActionContext.getServletContext().getResource(path);
+
+ if (resource == null) {
+ throw new TransformerException("Stylesheet " + path +
" not found in resources.");
+ }
+
+ LOG.debug("Preparing XSLT stylesheet templates: {}", path);
+
+ TransformerFactory factory = createTransformerFactory();
+ factory.setURIResolver(getURIResolver());
+ factory.setErrorListener(buildErrorListener());
+ templates = factory.newTemplates(new
StreamSource(resource.openStream()));
+ if (!noCache) {
+ templatesCache.put(path, templates);
+ }
}
-
- LOG.debug("Preparing XSLT stylesheet templates: {}", path);
-
- TransformerFactory factory = createTransformerFactory();
- factory.setURIResolver(getURIResolver());
- factory.setErrorListener(buildErrorListener());
- templates = factory.newTemplates(new
StreamSource(resource.openStream()));
- templatesCache.put(path, templates);
}
}
diff --git
a/plugins/xslt/src/test/java/org/apache/struts2/result/xslt/XSLTResultTest.java
b/plugins/xslt/src/test/java/org/apache/struts2/result/xslt/XSLTResultTest.java
index 51efb818a..127dc33c7 100644
---
a/plugins/xslt/src/test/java/org/apache/struts2/result/xslt/XSLTResultTest.java
+++
b/plugins/xslt/src/test/java/org/apache/struts2/result/xslt/XSLTResultTest.java
@@ -31,13 +31,21 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
+import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
+import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Unit test for {@link XSLTResult}.
@@ -167,6 +175,146 @@ public class XSLTResultTest extends StrutsTestCase {
TestCase.assertTrue(out.contains("<validators>"));
}
+ public void testConcurrentGetTemplatesCompilesOnce() throws Exception {
+ final AtomicInteger compileCount = new AtomicInteger(0);
+ final CountDownLatch compileStarted = new CountDownLatch(1);
+ final CountDownLatch releaseCompile = new CountDownLatch(1);
+
+ result = new XSLTResult() {
+ protected TransformerFactory createTransformerFactory() {
+ final TransformerFactory delegate =
super.createTransformerFactory();
+ return new TransformerFactory() {
+ public Transformer newTransformer(Source source) throws
TransformerConfigurationException {
+ return delegate.newTransformer(source);
+ }
+
+ public Transformer newTransformer() throws
TransformerConfigurationException {
+ return delegate.newTransformer();
+ }
+
+ public Templates newTemplates(Source source) throws
TransformerConfigurationException {
+ compileCount.incrementAndGet();
+ compileStarted.countDown();
+ try {
+ releaseCompile.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return delegate.newTemplates(source);
+ }
+
+ public Source getAssociatedStylesheet(Source source,
String media, String title, String charset) throws
TransformerConfigurationException {
+ return delegate.getAssociatedStylesheet(source, media,
title, charset);
+ }
+
+ public void setURIResolver(URIResolver resolver) {
+ delegate.setURIResolver(resolver);
+ }
+
+ public URIResolver getURIResolver() {
+ return delegate.getURIResolver();
+ }
+
+ public void setFeature(String name, boolean value) throws
TransformerConfigurationException {
+ delegate.setFeature(name, value);
+ }
+
+ public boolean getFeature(String name) {
+ return delegate.getFeature(name);
+ }
+
+ public void setAttribute(String name, Object value) {
+ delegate.setAttribute(name, value);
+ }
+
+ public Object getAttribute(String name) {
+ return delegate.getAttribute(name);
+ }
+
+ public void setErrorListener(ErrorListener listener) {
+ delegate.setErrorListener(listener);
+ }
+
+ public ErrorListener getErrorListener() {
+ return delegate.getErrorListener();
+ }
+ };
+ }
+ };
+
+ final String path = "XSLTResultTestDedup.xsl";
+ final ActionContext context = ActionContext.getContext();
+ final Templates[] compiled = new Templates[2];
+ final Exception[] errors = new Exception[2];
+
+ Thread first = new Thread(() -> {
+ ActionContext.bind(context);
+ try {
+ compiled[0] = result.getTemplates(path);
+ } catch (Exception e) {
+ errors[0] = e;
+ } finally {
+ ActionContext.clear();
+ }
+ });
+ first.start();
+
+ TestCase.assertTrue("First thread should have started compiling",
+ compileStarted.await(5, TimeUnit.SECONDS));
+
+ Thread second = new Thread(() -> {
+ ActionContext.bind(context);
+ try {
+ compiled[1] = result.getTemplates(path);
+ } catch (Exception e) {
+ errors[1] = e;
+ } finally {
+ ActionContext.clear();
+ }
+ });
+ second.start();
+
+ // Give the second thread time to reach the synchronized block and
park on
+ // the lock held by the first thread, so it hits the concurrent-miss
race
+ // rather than a plain cache hit.
+ Thread.sleep(200);
+
+ releaseCompile.countDown();
+
+ first.join(5000);
+ second.join(5000);
+
+ TestCase.assertNull(errors[0]);
+ TestCase.assertNull(errors[1]);
+ TestCase.assertEquals("Templates should be compiled exactly once
despite the concurrent miss",
+ 1, compileCount.get());
+ TestCase.assertSame("Both callers should observe the same cached
Templates instance",
+ compiled[0], compiled[1]);
+ }
+
+ public void testNoCacheDoesNotPollutePersistentCache() throws Exception {
+ final String path = "XSLTResultTestNoCachePollution.xsl";
+
+ // First, a normal (cached) compile populates the shared static cache.
+ result.setNoCache("false");
+ Templates cached = result.getTemplates(path);
+ TestCase.assertNotNull(cached);
+
+ // A noCache=true caller must still get a fresh compile...
+ result.setNoCache("true");
+ Templates fresh = result.getTemplates(path);
+ TestCase.assertNotNull(fresh);
+ TestCase.assertNotSame("noCache=true should always recompile rather
than reuse the cache",
+ cached, fresh);
+
+ // ...but must NOT overwrite the shared cache entry other, cached
callers rely on.
+ XSLTResult cachedCaller = new XSLTResult();
+ cachedCaller.setNoCache("false");
+ Templates stillCached = cachedCaller.getTemplates(path);
+ TestCase.assertSame("A noCache=true call must not pollute the shared
cache for cached callers",
+ cached, stillCached);
+ }
+
public void testTransform4WithBadDocumentInclude() {
result = new XSLTResult(){
protected URIResolver getURIResolver() {
diff --git a/plugins/xslt/src/test/resources/XSLTResultTestDedup.xsl
b/plugins/xslt/src/test/resources/XSLTResultTestDedup.xsl
new file mode 100644
index 000000000..f9582dfd1
--- /dev/null
+++ b/plugins/xslt/src/test/resources/XSLTResultTestDedup.xsl
@@ -0,0 +1,29 @@
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns="http://www.w3.org/TR/xhtml1/strict">
+
+ <xsl:template match="/">
+ <result/>
+ </xsl:template>
+
+</xsl:stylesheet>
diff --git a/plugins/xslt/src/test/resources/XSLTResultTestNoCachePollution.xsl
b/plugins/xslt/src/test/resources/XSLTResultTestNoCachePollution.xsl
new file mode 100644
index 000000000..f9582dfd1
--- /dev/null
+++ b/plugins/xslt/src/test/resources/XSLTResultTestNoCachePollution.xsl
@@ -0,0 +1,29 @@
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns="http://www.w3.org/TR/xhtml1/strict">
+
+ <xsl:template match="/">
+ <result/>
+ </xsl:template>
+
+</xsl:stylesheet>