From ce54b14a2924bd05174d48376d94b2c6ac35c9ea Mon Sep 17 00:00:00 2001 From: Thomas Nymand Date: Thu, 18 Jun 2026 15:50:59 +0200 Subject: [PATCH 1/2] REF-7: Support non-file resource URLs (WildFly/JBoss vfs:) in ResourceUtil (issue #80) ResourceUtil.getResourceAsFile only special-cased 'jar:' URLs and turned every other URL into a File via new File(url.toURI()). On WildFly/JBoss the classloader returns 'vfs:'/'vfszip:' URLs, for which that throws IllegalArgumentException ("URI scheme is not 'file'"), breaking resource loading during the authentication flow. Extract a package-private toFile(URL, String) that uses a plain 'file:' URL directly and stream-copies any other protocol (jar:, vfs:, vfszip:, ...) to a temporary file. Add a ResourceUtilTest case that simulates a WildFly vfs: URL and verifies it resolves to a readable file. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dk/gov/oio/saml/util/ResourceUtil.java | 46 +++++++++++++------ .../gov/oio/saml/util/ResourceUtilTest.java | 32 +++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java b/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java index 6896ed7..e1d4588 100644 --- a/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java +++ b/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java @@ -28,21 +28,8 @@ public static File getResourceAsFile(String resourceName) throws InternalExcepti throw new InternalException(String.format("Unable to load resource file '%s'", resourceName)); } try { - File file; URL url = ResourceUtil.getClassLoader().getResource(resourceName); - - if (url != null && "jar".equals(url.getProtocol())) { - // To access resources as files inside a jar we need to copy them to disk (use when unable to access as stream) - try (InputStream inputStream = ResourceUtil.getResourceAsStream(resourceName)) { - Path path = Files.createTempFile(resourceName, "tmp"); - java.nio.file.Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING); - file = path.toFile(); - } - } else if (url != null) { - file = new File(url.toURI()); - } else { - file = new File(resourceName); - } + File file = toFile(url, resourceName); if (!file.exists()) { throw new InternalException(String.format("Unable to load resource file '%s'", resourceName)); @@ -55,6 +42,37 @@ public static File getResourceAsFile(String resourceName) throws InternalExcepti } } + /** + * Resolve a classpath resource URL to a {@link File}. + *

+ * A plain {@code file:} URL is used directly. Resources that are not backed by a real + * filesystem file - inside a jar ({@code jar:}), or served by an application server + * virtual file system such as WildFly/JBoss ({@code vfs:}, {@code vfszip:}) - cannot be + * turned into a File with {@code new File(url.toURI())} (that throws + * {@code IllegalArgumentException: URI scheme is not "file"}), so they are copied to a + * temporary file through their stream instead. See issue #80. + * + * @param url URL returned by the classloader, or {@code null} when the resource is not on the classpath + * @param resourceName original resource name, used as the absolute/external path fallback when {@code url} is null + * @return file pointing to the resource + */ + static File toFile(URL url, String resourceName) throws URISyntaxException, IOException { + if (url != null && "file".equals(url.getProtocol())) { + return new File(url.toURI()); + } + + if (url != null) { + // Not a plain file (jar:, vfs:, vfszip:, ...) - copy to disk so it can be read as a File + try (InputStream inputStream = url.openStream()) { + Path path = Files.createTempFile(resourceName, "tmp"); + Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING); + return path.toFile(); + } + } + + return new File(resourceName); + } + /** * Returns an input stream for reading the specified resource. * @param resourceName Resource name from classpath or absolute path to resource diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java index 3d9b7e9..c798bb2 100644 --- a/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java +++ b/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java @@ -6,6 +6,9 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -44,6 +47,35 @@ void testGetResourceAsFileFromJar() throws InternalException { Assertions.assertTrue(file.isFile() && file.canRead()); } + @DisplayName("Test GetResourceAsFile handles non-file (WildFly vfs:) URLs - issue #80") + @Test + void testToFileFromVfsUrl() throws Exception { + // Simulate an application-server VFS resource URL (e.g. WildFly 'vfs:') backed by a + // real file. Such URLs are not 'file:' URLs, so the old new File(url.toURI()) approach + // throws; toFile must instead copy the resource to a readable temp file. + URLStreamHandler vfsHandler = new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override public void connect() { } + @Override public InputStream getInputStream() throws IOException { + return Files.newInputStream(externalPath); + } + }; + } + }; + URL vfsUrl = new URL(null, "vfs:/content/app.war/WEB-INF/classes/oiosaml.properties", vfsHandler); + + // The naive conversion that caused issue #80 fails for such URLs... + Assertions.assertThrows(IllegalArgumentException.class, () -> new File(vfsUrl.toURI())); + + // ...while toFile resolves it to a readable file holding the original content. + File file = ResourceUtil.toFile(vfsUrl, "oiosaml.properties"); + Assertions.assertTrue(file.isFile() && file.canRead()); + Assertions.assertEquals("this.is.a.test=1234", + new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); + } + @DisplayName("Test GetResourceAsStream from classpath") @Test void testGetResourceAsStreamFromClassPath() throws IOException, InternalException { From f4410bca596dd10d5caf965a97f26ebf833c0652 Mon Sep 17 00:00:00 2001 From: Thomas Nymand Date: Mon, 6 Jul 2026 14:19:35 +0200 Subject: [PATCH 2/2] REF-7: Handle subdirectory resource names when copying non-file URLs to temp file Files.createTempFile uses the resourceName as the temp-file prefix, and a prefix containing '/' (e.g. "config/oiosaml.properties" or a resource in WEB-INF/classes/config on WildFly VFS) is rejected with IllegalArgumentException. Use only the last path segment as the prefix and add an "oiosaml-" prefix to guarantee the minimum length. Addresses review feedback on #86. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dk/gov/oio/saml/util/ResourceUtil.java | 10 +++++-- .../gov/oio/saml/util/ResourceUtilTest.java | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java b/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java index e1d4588..4cdb229 100644 --- a/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java +++ b/oiosaml/src/main/java/dk/gov/oio/saml/util/ResourceUtil.java @@ -8,6 +8,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Enumeration; import java.util.HashMap; @@ -62,9 +63,14 @@ static File toFile(URL url, String resourceName) throws URISyntaxException, IOEx } if (url != null) { - // Not a plain file (jar:, vfs:, vfszip:, ...) - copy to disk so it can be read as a File + // Not a plain file (jar:, vfs:, vfszip:, ...) - copy to disk so it can be read as a File. + // Use only the last path segment as the temp-file prefix: a resourceName that refers to a + // file in a subdirectory (e.g. "config/oiosaml.properties") contains '/', which + // createTempFile rejects with IllegalArgumentException. The "oiosaml-" prefix also + // guarantees the minimum 3-character length required for the prefix. try (InputStream inputStream = url.openStream()) { - Path path = Files.createTempFile(resourceName, "tmp"); + String baseName = Paths.get(resourceName).getFileName().toString(); + Path path = Files.createTempFile("oiosaml-" + baseName, ".tmp"); Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING); return path.toFile(); } diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java index c798bb2..bbe7b4e 100644 --- a/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java +++ b/oiosaml/src/test/java/dk/gov/oio/saml/util/ResourceUtilTest.java @@ -76,6 +76,32 @@ protected URLConnection openConnection(URL u) { new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); } + @DisplayName("Test GetResourceAsFile handles non-file URLs whose resource is in a subdirectory - issue #80") + @Test + void testToFileFromVfsUrlInSubdirectory() throws Exception { + // A resource that lives in a subdirectory (e.g. "config/oiosaml.properties") has a + // resourceName containing '/'. createTempFile rejects such a value as a prefix + // (IllegalArgumentException: Invalid prefix or suffix), so toFile must use only the + // last path segment. Regression guard for the subdirectory case of issue #80. + URLStreamHandler vfsHandler = new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override public void connect() { } + @Override public InputStream getInputStream() throws IOException { + return Files.newInputStream(externalPath); + } + }; + } + }; + URL vfsUrl = new URL(null, "vfs:/content/app.war/WEB-INF/classes/config/oiosaml.properties", vfsHandler); + + File file = ResourceUtil.toFile(vfsUrl, "config/oiosaml.properties"); + Assertions.assertTrue(file.isFile() && file.canRead()); + Assertions.assertEquals("this.is.a.test=1234", + new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); + } + @DisplayName("Test GetResourceAsStream from classpath") @Test void testGetResourceAsStreamFromClassPath() throws IOException, InternalException {