From cf574943381480b01bb6c82846dc2e21ecb76bdb Mon Sep 17 00:00:00 2001 From: ren Date: Mon, 14 Feb 2022 22:16:09 -0500 Subject: [PATCH 1/4] Added ability to load resource using Classloader before attempting other means. Useful for app package as a custom java runtime image. --- .../goterl/resourceloader/ResourceLoader.java | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/goterl/resourceloader/ResourceLoader.java b/src/main/java/com/goterl/resourceloader/ResourceLoader.java index bcf6ff3..322cfcc 100644 --- a/src/main/java/com/goterl/resourceloader/ResourceLoader.java +++ b/src/main/java/com/goterl/resourceloader/ResourceLoader.java @@ -13,6 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.imageio.ImageIO; import java.io.*; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -70,6 +71,13 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws // Create the required directories. mainTempDir.mkdirs(); + // Check if we can access the resource using the classloader. + // This works for Java module. + File extracted = extractUsingClassLoader(mainTempDir, relativePath); + if (extracted != null) { + return extracted; + } + // Is the user loading resources that are // from inside a JAR? URL fullJarPathURL = getThePathToTheJarWeAreIn(outsideClass); @@ -77,7 +85,7 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws // Test if we are in a JAR and if we are // then do the following... if (isJarFile(fullJarPathURL)) { - File extracted = extractFromWithinAJarFile(fullJarPathURL, mainTempDir, relativePath); + extracted = extractFromWithinAJarFile(fullJarPathURL, mainTempDir, relativePath); if (extracted != null) { return extracted; } @@ -88,6 +96,67 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws return getFileFromFileSystem(relativePath, mainTempDir); } + // Copied from JDK 9 InputStream.transferTo() + private final int TRANSFER_BUFFER_SIZE = 8192; + private long transfer(final InputStream in, final OutputStream out) throws IOException { + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(out, "out"); + long transferred = 0; + byte[] buffer = new byte[TRANSFER_BUFFER_SIZE]; + int read; + while ((read = in.read(buffer, 0, TRANSFER_BUFFER_SIZE)) >= 0) { + out.write(buffer, 0, read); + transferred += read; + } + return transferred; + } + + /** + * Extract a resource using the traditional class loader. + * + * This is the method to use when an application is packaged as a custom Java runtime image (via Jlink). + * + * @param mainTempDir Temporarily directory to extract resource + * @param relativePath A relative path to a file or directory + * relative to the resource folder. + * @return The file you want to load. + * @throws IOException if the extraction of the resource fails (while transferring data) + */ + public File extractUsingClassLoader(File mainTempDir, String relativePath) throws IOException { + + final ClassLoader cl = getClass().getClassLoader(); + final InputStream is; + + // When run from Java module, getResourceAsStream must be used. The Java module system does provide a + // JrtFileSystem where Path are implemented with JrtPath, but, JrtPath cannot be converted to a File using. + // toFile() It is not supported and throws NotSupportedOperation exception if called. + + // FIXME: Does it have to be relative? + if (relativePath.charAt(0) == '/') { + is = cl.getResourceAsStream(relativePath.substring(1)); + } else { + is = cl.getResourceAsStream(relativePath); + } + + // Return null if resource can't be found that way. + if (is == null) { + return null; + } + + // Create the temp file under the provided temp directory. + final Path extractedLibraryFile = Files.createTempFile(mainTempDir.toPath(), "resource-loader", null); + + // Open an output stream to the tmp file then copy resource's bytes from the Java modules. + try (final OutputStream os = new BufferedOutputStream(new FileOutputStream(extractedLibraryFile.toFile()))) { + transfer(is, os); + } finally { + is.close(); + } + + // Finally, convert the path to a File. + return extractedLibraryFile.toFile(); + } + public File extractFromWithinAJarFile(URL jarPath, File mainTempDir, String relativePath) throws IOException, URISyntaxException { if (jarPath == null) { From d6f6b79f7f03e18056cb7d2b49ff3dd02170fd95 Mon Sep 17 00:00:00 2001 From: ren Date: Thu, 17 Feb 2022 12:16:54 -0500 Subject: [PATCH 2/4] formatting and doc --- .../java/com/goterl/resourceloader/ResourceLoader.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/goterl/resourceloader/ResourceLoader.java b/src/main/java/com/goterl/resourceloader/ResourceLoader.java index 322cfcc..2cbfa88 100644 --- a/src/main/java/com/goterl/resourceloader/ResourceLoader.java +++ b/src/main/java/com/goterl/resourceloader/ResourceLoader.java @@ -58,7 +58,7 @@ public class ResourceLoader { * Copies a file into a temporary directory regardless of * if it is in a JAR or not. * @param relativePath A relative path to a file or directory - * relative to the resources folder. + * relative to the resource folder. * @return The file or directory you want to load. * @throws IOException If at any point processing of the resource file fails. * @throws URISyntaxException If cannot find the resource file. @@ -73,7 +73,8 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws // Check if we can access the resource using the classloader. // This works for Java module. - File extracted = extractUsingClassLoader(mainTempDir, relativePath); + File extracted; + extracted = extractUsingClassLoader(mainTempDir, relativePath); if (extracted != null) { return extracted; } @@ -131,7 +132,7 @@ public File extractUsingClassLoader(File mainTempDir, String relativePath) throw // JrtFileSystem where Path are implemented with JrtPath, but, JrtPath cannot be converted to a File using. // toFile() It is not supported and throws NotSupportedOperation exception if called. - // FIXME: Does it have to be relative? + // It should already be relative but just in case. if (relativePath.charAt(0) == '/') { is = cl.getResourceAsStream(relativePath.substring(1)); } else { From dce4e1a2624a917d7cad89a8cf12a34fc5769d4d Mon Sep 17 00:00:00 2001 From: ren Date: Thu, 17 Feb 2022 14:06:41 -0500 Subject: [PATCH 3/4] added check for modular app to keep library previous behavior (backward compatbility) --- .../goterl/resourceloader/ResourceLoader.java | 69 ++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/goterl/resourceloader/ResourceLoader.java b/src/main/java/com/goterl/resourceloader/ResourceLoader.java index 2cbfa88..c4fb9dc 100644 --- a/src/main/java/com/goterl/resourceloader/ResourceLoader.java +++ b/src/main/java/com/goterl/resourceloader/ResourceLoader.java @@ -13,9 +13,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.imageio.ImageIO; import java.io.*; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.FileChannel; @@ -71,18 +71,24 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws // Create the required directories. mainTempDir.mkdirs(); - // Check if we can access the resource using the classloader. - // This works for Java module. + // Is the user loading resources that are + // from inside a JAR? + URL fullJarPathURL = getThePathToTheJarWeAreIn(outsideClass); + File extracted; + + // Check if we can access the file using the classloader. + // This works when this library is used in a Java modular project and merged into a single module. + // https://github.com/beryx/badass-jlink-plugin. + // + // This call will quickly bail out with null if we are not running in a modular runtime image. + // + // NOTE: this won't work for directory. extracted = extractUsingClassLoader(mainTempDir, relativePath); if (extracted != null) { return extracted; } - // Is the user loading resources that are - // from inside a JAR? - URL fullJarPathURL = getThePathToTheJarWeAreIn(outsideClass); - // Test if we are in a JAR and if we are // then do the following... if (isJarFile(fullJarPathURL)) { @@ -98,40 +104,68 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws } // Copied from JDK 9 InputStream.transferTo() - private final int TRANSFER_BUFFER_SIZE = 8192; - private long transfer(final InputStream in, final OutputStream out) throws IOException { + private void transfer(final InputStream in, final OutputStream out) throws IOException { Objects.requireNonNull(in, "in"); Objects.requireNonNull(out, "out"); - long transferred = 0; + int TRANSFER_BUFFER_SIZE = 8192; byte[] buffer = new byte[TRANSFER_BUFFER_SIZE]; int read; while ((read = in.read(buffer, 0, TRANSFER_BUFFER_SIZE)) >= 0) { out.write(buffer, 0, read); - transferred += read; } - return transferred; + } + + /** + * Returns whether we are running from a modular runtime image without involving Java 9 APIs. + * Resource loaded from a JrtFileSystem will have the "jrt" protocol/scheme in their URL/URI. + * SEE JEP220. + * + * @param relativePath any file relative path to test + * @return true if we are running from a modular runtime image, false otherwise + */ + private boolean isModularRuntimeImage(String relativePath) { + final URL url = getClass().getClassLoader().getResource(relativePath); + if (url != null && Objects.equals(url.getProtocol(), "jrt")) { + return true; + } + // Fall back to false if not a JRT or if getResource return null. + return false; } /** * Extract a resource using the traditional class loader. * - * This is the method to use when an application is packaged as a custom Java runtime image (via Jlink). + * This is the method to use when an application is packaged as a modular runtime image (via Jlink). + * + * NOTE: This method is unable to extract a directory. * * @param mainTempDir Temporarily directory to extract resource - * @param relativePath A relative path to a file or directory - * relative to the resource folder. + * @param relativePath A relative path to a file relative to the resource folder. * @return The file you want to load. * @throws IOException if the extraction of the resource fails (while transferring data) */ public File extractUsingClassLoader(File mainTempDir, String relativePath) throws IOException { + // Bail out if we are not running from a modular runtime image. This is to keep backward compatibility + // with project already using this library and loading/copying directories. + if (!isModularRuntimeImage(relativePath)) { + return null; + } + final ClassLoader cl = getClass().getClassLoader(); final InputStream is; - // When run from Java module, getResourceAsStream must be used. The Java module system does provide a + // When run from modular runtime image, getResourceAsStream must be used. The Java module system does provide a // JrtFileSystem where Path are implemented with JrtPath, but, JrtPath cannot be converted to a File using. // toFile() It is not supported and throws NotSupportedOperation exception if called. + // As per JEP220, the URI scheme for modular Runtime image is: + // jrt:/[$MODULE[/$PATH]] + + // If this library is used in a modular project using jlink and the https://github.com/beryx/badass-jlink-plugin + // plugin, then it will be merged into a single jar before being put into the host application module. + // Therefore, using this class' class loader will load resources relative to the merged module. + // It should already be relative but just in case. if (relativePath.charAt(0) == '/') { is = cl.getResourceAsStream(relativePath.substring(1)); @@ -145,7 +179,8 @@ public File extractUsingClassLoader(File mainTempDir, String relativePath) throw } // Create the temp file under the provided temp directory. - final Path extractedLibraryFile = Files.createTempFile(mainTempDir.toPath(), "resource-loader", null); + final Path extractedLibraryFile = + Files.createTempFile(mainTempDir.toPath(), "resource-loader", null); // Open an output stream to the tmp file then copy resource's bytes from the Java modules. try (final OutputStream os = new BufferedOutputStream(new FileOutputStream(extractedLibraryFile.toFile()))) { From e63b48f337d77c3a51766a65ac2fc1f9ca1449c2 Mon Sep 17 00:00:00 2001 From: ren Date: Thu, 17 Feb 2022 14:08:00 -0500 Subject: [PATCH 4/4] unused import --- src/main/java/com/goterl/resourceloader/ResourceLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/goterl/resourceloader/ResourceLoader.java b/src/main/java/com/goterl/resourceloader/ResourceLoader.java index c4fb9dc..9372712 100644 --- a/src/main/java/com/goterl/resourceloader/ResourceLoader.java +++ b/src/main/java/com/goterl/resourceloader/ResourceLoader.java @@ -15,7 +15,6 @@ import java.io.*; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.FileChannel;