From 368e726505d7e2db072734f0e9d3a783fada674a Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Wed, 28 Oct 2026 21:44:08 +0000 Subject: [PATCH] Read jar resources without the JVM jar: cache JarHolder kept a JarFile from the JVM-wide jar: cache open for the life of the loader, so the jar stayed locked on Windows and was read stale once rewritten (JDK-8239054). It now indexes the jar once and reads each resource through its own uncached connection. Do not switch back to a cached JarURLConnection. --- .../plexus/resource/loader/JarHolder.java | 94 +++++++++---------- .../loader/JarResourceLoaderTest.java | 72 ++++++++++++++ 2 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 src/test/java/org/codehaus/plexus/resource/loader/JarResourceLoaderTest.java diff --git a/src/main/java/org/codehaus/plexus/resource/loader/JarHolder.java b/src/main/java/org/codehaus/plexus/resource/loader/JarHolder.java index 48f36e1..3e4ac9b 100644 --- a/src/main/java/org/codehaus/plexus/resource/loader/JarHolder.java +++ b/src/main/java/org/codehaus/plexus/resource/loader/JarHolder.java @@ -30,8 +30,12 @@ import java.net.JarURLConnection; import java.net.URI; import java.net.URL; +import java.net.URLConnection; +import java.util.Collections; import java.util.Enumeration; import java.util.Hashtable; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -47,72 +51,63 @@ public class JarHolder { private final String urlpath; - private JarFile theJar = null; - - private JarURLConnection conn = null; + private final Set entries; public JarHolder(String urlpath) { this.urlpath = urlpath; - try { - URL url = new URL(urlpath); - - conn = (JarURLConnection) url.openConnection(); - - conn.setAllowUserInteraction(false); - - conn.setDoInput(true); - - conn.setDoOutput(false); - - conn.connect(); + Set names = Collections.emptySet(); + // The jar is indexed once and every resource is read from a connection of its own, all without the + // JVM-wide jar: cache: a cached JarFile is never closed, so the jar stays locked on Windows and is read + // stale once it is rewritten (JDK-8239054). + try (JarFile jar = openJar(new URL(urlpath))) { + names = new LinkedHashSet<>(); + Enumeration all = jar.entries(); + while (all.hasMoreElements()) { + JarEntry je = all.nextElement(); - theJar = conn.getJarFile(); + // We don't map plain directory entries + if (!je.isDirectory()) { + names.add(je.getName()); + } + } } catch (IOException ioe) { } + this.entries = names; } - public void close() { - try { - theJar.close(); - } catch (Exception e) { - } - - theJar = null; + private static JarFile openJar(URL url) throws IOException { + JarURLConnection conn = (JarURLConnection) url.openConnection(); + conn.setUseCaches(false); + return conn.getJarFile(); + } - conn = null; + private InputStream openEntry(String name) throws IOException { + URLConnection conn = new URL(urlpath + name).openConnection(); + conn.setUseCaches(false); + return conn.getInputStream(); } - public InputStream getResource(String theentry) throws ResourceNotFoundException { - InputStream data = null; + /** + * Kept for compatibility: the jar is no longer held open, so there is nothing to close. + */ + public void close() {} + public InputStream getResource(String theentry) throws ResourceNotFoundException { + if (!entries.contains(theentry)) { + return null; + } try { - JarEntry entry = theJar.getJarEntry(theentry); - - if (entry != null) { - data = theJar.getInputStream(entry); - } + return openEntry(theentry); } catch (Exception fnfe) { throw new ResourceNotFoundException(fnfe.getMessage()); } - - return data; } public Hashtable getEntries() { Hashtable allEntries = new Hashtable<>(559); - - if (theJar != null) { - Enumeration all = theJar.entries(); - - while (all.hasMoreElements()) { - JarEntry je = all.nextElement(); - - // We don't map plain directory entries - if (!je.isDirectory()) { - allEntries.put(je.getName(), this.urlpath); - } - } + for (String name : entries) { + allEntries.put(name, this.urlpath); } return allEntries; } @@ -122,8 +117,7 @@ public String getUrlPath() { } public PlexusResource getPlexusResource(final String name) { - final JarEntry entry = theJar.getJarEntry(name); - if (entry == null) { + if (!entries.contains(name)) { return null; } return new PlexusResource() { @@ -134,12 +128,12 @@ public File getFile() { @Override public InputStream getInputStream() throws IOException { - return theJar.getInputStream(entry); + return openEntry(name); } @Override public String getName() { - return conn.getURL() + name; + return urlpath + name; } @Override @@ -149,7 +143,7 @@ public URI getURI() { @Override public URL getURL() throws IOException { - return new URL(conn.getJarFileURL(), name); + return new URL(urlpath + name); } }; } diff --git a/src/test/java/org/codehaus/plexus/resource/loader/JarResourceLoaderTest.java b/src/test/java/org/codehaus/plexus/resource/loader/JarResourceLoaderTest.java new file mode 100644 index 0000000..a395abf --- /dev/null +++ b/src/test/java/org/codehaus/plexus/resource/loader/JarResourceLoaderTest.java @@ -0,0 +1,72 @@ +package org.codehaus.plexus.resource.loader; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import org.codehaus.plexus.resource.PlexusResource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JarResourceLoaderTest { + @TempDir + Path tempDir; + + @Test + void readsRewrittenJar() throws Exception { + Path jar = tempDir.resolve("resources.jar"); + writeJar(jar, "first"); + assertEquals("first", read(jar)); + + // Rewritten in place, as a reactor module or a reinstalled artifact is. + writeJar(jar, "second, and longer than the first"); + assertEquals("second, and longer than the first", read(jar)); + + // The jar is not held open, so it can be deleted (fails on Windows while it is). + Files.delete(jar); + } + + @Test + void urlPointsIntoJar() throws Exception { + Path jar = tempDir.resolve("resources.jar"); + writeJar(jar, "content"); + assertEquals( + "jar:" + jar.toUri() + "!/config/rules.txt", + resource(jar).getURL().toString()); + } + + private static PlexusResource resource(Path jar) throws Exception { + JarResourceLoader loader = new JarResourceLoader(); + loader.addSearchPath("jar:" + jar.toUri()); + return loader.getResource("config/rules.txt"); + } + + private static String read(Path jar) throws Exception { + PlexusResource resource = resource(jar); + try (InputStream in = resource.getInputStream()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[256]; + for (int n; (n = in.read(buffer)) != -1; ) { + bytes.write(buffer, 0, n); + } + return new String(bytes.toByteArray(), StandardCharsets.UTF_8); + } + } + + private static void writeJar(Path jar, String content) throws IOException { + try (OutputStream out = Files.newOutputStream(jar); + JarOutputStream jarOut = new JarOutputStream(out)) { + jarOut.putNextEntry(new ZipEntry("config/rules.txt")); + jarOut.write(content.getBytes(StandardCharsets.UTF_8)); + jarOut.closeEntry(); + } + } +}