From b04d03228c18ebdf14a015ac3aa24d6de444fca3 Mon Sep 17 00:00:00 2001 From: Jaehong Park <105151063+PHJ2000@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:33:07 +0900 Subject: [PATCH] Use JAR entry metadata to identify template directories Signed-off-by: Jaehong Park <105151063+PHJ2000@users.noreply.github.com> --- .../resolver/ClasspathResolver.java | 35 ++++---- .../resolver/ClasspathJarResolverTest.java | 89 +++++++++++++++++++ 2 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 compiler/src/test/java/com/github/mustachejava/resolver/ClasspathJarResolverTest.java diff --git a/compiler/src/main/java/com/github/mustachejava/resolver/ClasspathResolver.java b/compiler/src/main/java/com/github/mustachejava/resolver/ClasspathResolver.java index 579e6817..fd73a1d9 100644 --- a/compiler/src/main/java/com/github/mustachejava/resolver/ClasspathResolver.java +++ b/compiler/src/main/java/com/github/mustachejava/resolver/ClasspathResolver.java @@ -3,9 +3,12 @@ import com.github.mustachejava.MustacheResolver; import java.io.*; +import java.net.JarURLConnection; import java.net.URI; import java.net.URL; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; +import java.util.jar.JarEntry; /** * MustacheResolver implementation that resolves mustache files from the classpath. @@ -35,29 +38,27 @@ public Reader getReader(String resourceName) { String normalizeResourceName = URI.create(fullResourceName).normalize().getPath(); URL resource = ccl.getResource(normalizeResourceName); - if (resource != null) { - if (resource.getProtocol().equals("jar")) { - if (normalizeResourceName.endsWith("/")) { - // This is a directory - return null; - } else if (ccl.getResource(normalizeResourceName + "/") != null) { - // This is a directory - return null; - } - } else if (resource.getProtocol().equals("file")) { - if (new File(resource.getPath()).isDirectory()) { - // This is a directory - return null; - } - } - } else { + if (resource == null) { resource = ClasspathResolver.class.getClassLoader().getResource(normalizeResourceName); } if (resource != null) { try { - return new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8)); + if (resource.getProtocol().equals("jar") && normalizeResourceName.endsWith("/")) { + return null; + } + if (resource.getProtocol().equals("file") && new File(resource.getPath()).isDirectory()) { + return null; + } + URLConnection connection = resource.openConnection(); + if (connection instanceof JarURLConnection) { + JarEntry entry = ((JarURLConnection) connection).getJarEntry(); + if (entry == null || entry.isDirectory()) { + return null; + } + } + return new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); } catch (IOException e) { return null; } diff --git a/compiler/src/test/java/com/github/mustachejava/resolver/ClasspathJarResolverTest.java b/compiler/src/test/java/com/github/mustachejava/resolver/ClasspathJarResolverTest.java new file mode 100644 index 00000000..2cfd4429 --- /dev/null +++ b/compiler/src/test/java/com/github/mustachejava/resolver/ClasspathJarResolverTest.java @@ -0,0 +1,89 @@ +package com.github.mustachejava.resolver; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.Reader; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class ClasspathJarResolverTest { + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void readsFileWhenClassLoaderAlsoResolvesTrailingSlash() throws Exception { + assertResource("templates/view.mustache", true, "hello"); + } + + @Test + public void readsFileWithOrdinaryJarClassLoader() throws Exception { + assertResource("templates/view.mustache", false, "hello"); + } + + @Test + public void rejectsDirectoryWithoutTrailingSlash() throws Exception { + assertResource("templates", false, null); + assertResource("templates", true, null); + } + + @Test + public void rejectsDirectoryWithTrailingSlash() throws Exception { + assertResource("templates/", false, null); + assertResource("templates/", true, null); + } + + @Test + public void missingJarResourceReturnsNull() throws Exception { + assertResource("missing.mustache", false, null); + assertResource("missing.mustache", true, null); + } + + private void assertResource(String resource, boolean normalizeTrailingSlash, String expected) + throws Exception { + File jar = temporary.newFile(); + try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jar))) { + output.putNextEntry(new JarEntry("templates/")); + output.closeEntry(); + output.putNextEntry(new JarEntry("templates/view.mustache")); + output.write("hello".getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } + ClassLoader original = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader loader = new URLClassLoader(new URL[] {jar.toURI().toURL()}, null) { + @Override + public URL getResource(String name) { + if (normalizeTrailingSlash && name.endsWith("/")) { + name = name.substring(0, name.length() - 1); + } + return super.getResource(name); + } + }) { + Thread.currentThread().setContextClassLoader(loader); + try (Reader reader = new ClasspathResolver().getReader(resource)) { + if (expected == null) { + assertNull(reader); + } else { + assertNotNull(reader); + StringBuilder text = new StringBuilder(); + for (int c; (c = reader.read()) != -1;) { + text.append((char) c); + } + assertEquals(expected, text.toString()); + } + } + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } +}