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..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; @@ -28,21 +29,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 +43,42 @@ 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. + // 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()) { + String baseName = Paths.get(resourceName).getFileName().toString(); + Path path = Files.createTempFile("oiosaml-" + baseName, ".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..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 @@ -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,61 @@ 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 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 {