Skip to content

Commit 33a5d7f

Browse files
committed
Prevent noCache from polluting shared template cache; add dedup and noCache regression tests
1 parent 6faedcf commit 33a5d7f

4 files changed

Lines changed: 209 additions & 1 deletion

File tree

plugins/xslt/src/main/java/org/apache/struts2/result/xslt/XSLTResult.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,9 @@ protected Templates getTemplates(final String path) throws TransformerException,
294294
factory.setURIResolver(getURIResolver());
295295
factory.setErrorListener(buildErrorListener());
296296
templates = factory.newTemplates(new StreamSource(resource.openStream()));
297-
templatesCache.put(path, templates);
297+
if (!noCache) {
298+
templatesCache.put(path, templates);
299+
}
298300
}
299301
}
300302
}

plugins/xslt/src/test/java/org/apache/struts2/result/xslt/XSLTResultTest.java

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@
3131
import org.springframework.mock.web.MockHttpServletResponse;
3232
import org.springframework.mock.web.MockServletContext;
3333

34+
import javax.xml.transform.ErrorListener;
3435
import javax.xml.transform.Source;
36+
import javax.xml.transform.Templates;
37+
import javax.xml.transform.Transformer;
38+
import javax.xml.transform.TransformerConfigurationException;
3539
import javax.xml.transform.TransformerException;
40+
import javax.xml.transform.TransformerFactory;
3641
import javax.xml.transform.URIResolver;
3742
import javax.xml.transform.stream.StreamSource;
3843
import java.util.ArrayList;
3944
import java.util.Arrays;
4045
import java.util.List;
46+
import java.util.concurrent.CountDownLatch;
47+
import java.util.concurrent.TimeUnit;
48+
import java.util.concurrent.atomic.AtomicInteger;
4149

4250
/**
4351
* Unit test for {@link XSLTResult}.
@@ -167,6 +175,146 @@ public Source resolve(String href, String base) throws TransformerException {
167175
TestCase.assertTrue(out.contains("<validators>"));
168176
}
169177

178+
public void testConcurrentGetTemplatesCompilesOnce() throws Exception {
179+
final AtomicInteger compileCount = new AtomicInteger(0);
180+
final CountDownLatch compileStarted = new CountDownLatch(1);
181+
final CountDownLatch releaseCompile = new CountDownLatch(1);
182+
183+
result = new XSLTResult() {
184+
protected TransformerFactory createTransformerFactory() {
185+
final TransformerFactory delegate = super.createTransformerFactory();
186+
return new TransformerFactory() {
187+
public Transformer newTransformer(Source source) throws TransformerConfigurationException {
188+
return delegate.newTransformer(source);
189+
}
190+
191+
public Transformer newTransformer() throws TransformerConfigurationException {
192+
return delegate.newTransformer();
193+
}
194+
195+
public Templates newTemplates(Source source) throws TransformerConfigurationException {
196+
compileCount.incrementAndGet();
197+
compileStarted.countDown();
198+
try {
199+
releaseCompile.await();
200+
} catch (InterruptedException e) {
201+
Thread.currentThread().interrupt();
202+
}
203+
return delegate.newTemplates(source);
204+
}
205+
206+
public Source getAssociatedStylesheet(Source source, String media, String title, String charset) throws TransformerConfigurationException {
207+
return delegate.getAssociatedStylesheet(source, media, title, charset);
208+
}
209+
210+
public void setURIResolver(URIResolver resolver) {
211+
delegate.setURIResolver(resolver);
212+
}
213+
214+
public URIResolver getURIResolver() {
215+
return delegate.getURIResolver();
216+
}
217+
218+
public void setFeature(String name, boolean value) throws TransformerConfigurationException {
219+
delegate.setFeature(name, value);
220+
}
221+
222+
public boolean getFeature(String name) {
223+
return delegate.getFeature(name);
224+
}
225+
226+
public void setAttribute(String name, Object value) {
227+
delegate.setAttribute(name, value);
228+
}
229+
230+
public Object getAttribute(String name) {
231+
return delegate.getAttribute(name);
232+
}
233+
234+
public void setErrorListener(ErrorListener listener) {
235+
delegate.setErrorListener(listener);
236+
}
237+
238+
public ErrorListener getErrorListener() {
239+
return delegate.getErrorListener();
240+
}
241+
};
242+
}
243+
};
244+
245+
final String path = "XSLTResultTestDedup.xsl";
246+
final ActionContext context = ActionContext.getContext();
247+
final Templates[] compiled = new Templates[2];
248+
final Exception[] errors = new Exception[2];
249+
250+
Thread first = new Thread(() -> {
251+
ActionContext.bind(context);
252+
try {
253+
compiled[0] = result.getTemplates(path);
254+
} catch (Exception e) {
255+
errors[0] = e;
256+
} finally {
257+
ActionContext.clear();
258+
}
259+
});
260+
first.start();
261+
262+
TestCase.assertTrue("First thread should have started compiling",
263+
compileStarted.await(5, TimeUnit.SECONDS));
264+
265+
Thread second = new Thread(() -> {
266+
ActionContext.bind(context);
267+
try {
268+
compiled[1] = result.getTemplates(path);
269+
} catch (Exception e) {
270+
errors[1] = e;
271+
} finally {
272+
ActionContext.clear();
273+
}
274+
});
275+
second.start();
276+
277+
// Give the second thread time to reach the synchronized block and park on
278+
// the lock held by the first thread, so it hits the concurrent-miss race
279+
// rather than a plain cache hit.
280+
Thread.sleep(200);
281+
282+
releaseCompile.countDown();
283+
284+
first.join(5000);
285+
second.join(5000);
286+
287+
TestCase.assertNull(errors[0]);
288+
TestCase.assertNull(errors[1]);
289+
TestCase.assertEquals("Templates should be compiled exactly once despite the concurrent miss",
290+
1, compileCount.get());
291+
TestCase.assertSame("Both callers should observe the same cached Templates instance",
292+
compiled[0], compiled[1]);
293+
}
294+
295+
public void testNoCacheDoesNotPollutePersistentCache() throws Exception {
296+
final String path = "XSLTResultTestNoCachePollution.xsl";
297+
298+
// First, a normal (cached) compile populates the shared static cache.
299+
result.setNoCache("false");
300+
Templates cached = result.getTemplates(path);
301+
TestCase.assertNotNull(cached);
302+
303+
// A noCache=true caller must still get a fresh compile...
304+
result.setNoCache("true");
305+
Templates fresh = result.getTemplates(path);
306+
TestCase.assertNotNull(fresh);
307+
TestCase.assertNotSame("noCache=true should always recompile rather than reuse the cache",
308+
cached, fresh);
309+
310+
// ...but must NOT overwrite the shared cache entry other, cached callers rely on.
311+
XSLTResult cachedCaller = new XSLTResult();
312+
cachedCaller.setNoCache("false");
313+
Templates stillCached = cachedCaller.getTemplates(path);
314+
TestCase.assertSame("A noCache=true call must not pollute the shared cache for cached callers",
315+
cached, stillCached);
316+
}
317+
170318
public void testTransform4WithBadDocumentInclude() {
171319
result = new XSLTResult(){
172320
protected URIResolver getURIResolver() {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!--
2+
/*
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
*/
20+
-->
21+
<xsl:stylesheet version="1.0"
22+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
23+
xmlns="http://www.w3.org/TR/xhtml1/strict">
24+
25+
<xsl:template match="/">
26+
<result/>
27+
</xsl:template>
28+
29+
</xsl:stylesheet>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!--
2+
/*
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
*/
20+
-->
21+
<xsl:stylesheet version="1.0"
22+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
23+
xmlns="http://www.w3.org/TR/xhtml1/strict">
24+
25+
<xsl:template match="/">
26+
<result/>
27+
</xsl:template>
28+
29+
</xsl:stylesheet>

0 commit comments

Comments
 (0)