Skip to content

Load file resource when used in a modular runtime image application. #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions src/main/java/com/goterl/resourceloader/ResourceLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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.
Expand All @@ -74,10 +74,24 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws
// 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;
}

// 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;
}
Expand All @@ -88,6 +102,96 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws
return getFileFromFileSystem(relativePath, mainTempDir);
}

// Copied from JDK 9 InputStream.transferTo()
private void transfer(final InputStream in, final OutputStream out) throws IOException {
Objects.requireNonNull(in, "in");
Objects.requireNonNull(out, "out");
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);
}
}

/**
* 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 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 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 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));
} 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) {
Expand Down