How to Get List of Classpath Resources In Nested Jar? - java

I have a Maven project, and I am packaging it as a single fat jar using the One-Jar Maven plugin. According to this page on the One-Jar site documentation resource loading must be done in a special manner with One-Jar packaged apps, however that documentation does not cover how to list the resources in a manner which does not depend on their custom class loader.
My question is this: how to I list the contents of the root of my classpath within an inner jar in a packaging agnostic manner, meaning not assuming it will always be in such special packaging? As covered in this SO question, Spring's PathMatchingResourcePatternResolver will not work because Spring's DefaultResourceLoader ignores the classloader.
NOTE: The closest SO question I could find was this question, but this does not address the listing of classpath resources from nested jars.

It can be done in a FAT jar. Your own answer stating it's not possible has a link to a page that shows it's possible! It's not that insane of a solution. Here's a slightly modified version:
private List<String> getResourceFiles(String path) throws IOException
{
List<String> files = new ArrayList<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL dirURL = classLoader.getResource(path);
if (dirURL.getProtocol().equals("jar"))
{
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!"));
try (final JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")))
{
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
String name = entries.nextElement().getName();
if (name.startsWith(path + "/") && !name.endsWith("/"))
{
files.add(name);
}
}
}
return files;
}
// return some other way when not a JAR
}
This version will return the full resource path of any file under the given resource path, recursively. For example, an input of config/rest would return files under that resource folder as:
config/rest/options.properties
config/rest/samples/data.json
With those values, one can get the resource with ClassLoader::getResource, or get the contents of the resource with ClassLoader::getResourceAsStream.

After reviewing many articles, and blogs posts, web pages, site and the Spring source code, I had concluded this is just a weakness in Java and is not possible in any sane manner.

Related

Java: Getting resource path of the main app instead of jar's

A lot has been discussed already here about getting a resource.
If there is already a solution - please point me to it because I couldn't find.
I have a program which uses several jars.
To one of the jars I added a properties file under main/resources folder.
I've added the following method to the jar project in order to to read it:
public void loadAppPropertiesFile() {
try {
Properties prop = new Properties();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String resourcePath = this.getClass().getClassLoader().getResource("").getPath();
InputStream stream = loader.getResourceAsStream(resourcePath + "\\entities.properties");
prop.load(stream);
String default_ssl = prop.getProperty("default_ssl");
}catch (Exception e){
}
}
The problem (?) is that resourcePath gives me a path to the target\test-clasess but under the calling application directory although the loading code exists in the jar!
This the jar content:
The jar is added to the main project by maven dependency.
How can I overcome this state and read the jar resource file?
Thanks!
I would suggest using the classloader used to load the class, not the context classloader.
Then, you have two options to get at a resource at the root of the jar file:
Use Class.getResourceAsStream, passing in an absolute path (leading /)
Use ClassLoader.getResourceAsStream, passing in a relative path (just "entities.properties")
So either of:
InputStream stream = getClass().getResourceAsStream("/entities.properties");
InputStream stream = getClass().getClassLoader().getResourceAsStream("entities.properties");
Personally I'd use the first option as it's briefer and just as clear.
Can you try this:
InputStream stream = getClass().getClassLoader().getResourceAsStream("entities.properties")

Java load jar files from directory

I currently develop an open-source project where people may add their own .jar to extend the included features. However, I'm stuck with how to load the classes in the jar.
I have an abstract class which has an abstract method onEnable() and some getter that provides some objects to work with the application. The plugin needs the subclass my plugin-class BasePlugin. The jar should be added to /plugins and thus I want all *.jar files in the /plugins folder to be loaded when the application starts.
The problem I'm running to now is that, of all the approaches I found, I need to declare a classpath of the classes in the jar file, which I do not know. Neither do I know the name of the jar file. Thus, I need to scan the /plugins folder for any *.jar file and load the corresponding class inside the jar which implements BasePlugin and invoke the onEnable() method.
The basic idea is too...
Read all the files in a specific directory
Convert the File reference to a URL for each result
Use a URLClassLoader, seeded with the URL results to load each result
Use URLClassLoader#findResources to find all the match resources with a specific name
Iterate over the matching resources and load each one, which should give, at least, the "entry point" class name
Load the class specified by the "entry point" property
For example...
public List<PluginClass> loadPlugins() throws MalformedURLException, IOException, ClassNotFoundException {
File plugins[] = new File("./Plugins").listFiles(new FileFilter() {
#Override
public boolean accept(File file) {
return file.getName().endsWith(".jar");
}
});
List<URL> plugInURLs = new ArrayList<>(plugins.length);
for (File plugin : plugins) {
plugInURLs.add(plugin.toURI().toURL());
}
URLClassLoader loader = new URLClassLoader(plugInURLs.toArray(new URL[0]));
Enumeration<URL> resources = loader.findResources("/META-INFO/Plugin.properties");
List<PluginClass> classes = new ArrayList<>(plugInURLs.size());
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
Properties properties = new Properties();
try (InputStream is = resource.openStream()) {
properties.load(is);
String className = properties.getProperty("enrty-point");
PluginClass pluginClass = loader.loadClass(className);
classes.add(pluginClass);
}
}
return classes
}
nb: I've not run this, but this is the "basic
SpigotMC uses JAR files as plugins as well, inside the jar is a plugin.yaml file that stores extra information about the plugin including the classpath. You don't need to use a YAML file, instead you could use something like JSON or even a plain text file.
The YAML file is inside the jar and can be accessed by using some of the methods explained here. You can then get the classpath property and then load the jar using the methods explained here. Extra information can be stored about the plugin such as the name, version, and dependencies.
Java already has a class for this: ServiceLoader.
The ServiceLoader class was introduced with Java 6, but the “SPI jar” concept is actually as old as Java 1.3. The idea is that each jar contains a short text file that describes its implementations of a particular service provider interface.
For instance, if a .jar file contains two subclasses of BasePlugin named FooPlugin and BarPlugin, the .jar file would also contain the following entry:
META-INF/services/com.example.BasePlugin
And that jar entry would be a text file, containing the following lines:
com.myplugins.FooPlugin
com.myplugins.BarPlugin
Your project would scan for the plugins by creating a ClassLoader that reads from the plugins directory:
Collection<URL> urlList = new ArrayList<>();
Path pluginsDir = Paths.get(
System.getProperty("user.home"), "plugins");
try (DirectoryStream<Path> jars =
Files.newDirectoryStream(pluginsDir, "*.jar")) {
for (Path jar : jars) {
urlList.add(jar.toUri().toURL());
}
}
URL[] urls = urlList.toArray(new URL[0]);
ClassLoader pluginClassLoader = new URLClassLoader(urls,
BasePlugin.class.getClassLoader());
ServiceLoader<BasePlugin> plugins =
ServiceLoader.load(BasePlugin.class, pluginClassLoader);
for (BasePlugin plugin : plugins) {
plugin.onEnable();
// etc.
}
An additional advantage of using ServiceLoader is that your code will be capable of working with modules, a more complete form of code encapsulation introduced with Java 9 which offers increased security.
There is an example here it may be helpful. Also, you should take a look at OSGi.

How to read several resource files with the same name from different JARs?

If there are two JAR files in the classpath, both containing a resource named "config.properties" in its root. Is there a way to retrieve both files similar to getClass().getResourceAsStream()? The order is not relevant.
An alternative would be to load every property file in the class path that match certain criterias, if this is possible at all.
You need ClassLoader.getResources(name)
(or the static version ClassLoader.getSystemResources(name)).
But unfortunately there's a known issue with resources that are not inside a "directory". E.g. foo/bar.txt is fine, but bar.txt can be a problem. This is described well in the Spring Reference, although it is by no means a Spring-specific problem.
Update:
Here's a helper method that returns a list of InputStreams:
public static List<InputStream> loadResources(
final String name, final ClassLoader classLoader) throws IOException {
final List<InputStream> list = new ArrayList<InputStream>();
final Enumeration<URL> systemResources =
(classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader)
.getResources(name);
while (systemResources.hasMoreElements()) {
list.add(systemResources.nextElement().openStream());
}
return list;
}
Usage:
List<InputStream> resources = loadResources("config.properties", classLoader);
// or:
List<InputStream> resources = loadResources("config.properties", null);
jar files are zip files.
Open the file using java.util.zip.ZipFile
Then enumerate its entries looking for the properties file you want.
When you have the entry you can get its stream with .getInputStream()

JarInputStream: getNextJarEntry always returns null

I have an I18n helper class that can find out the available Locales by looking at the name of the files inside the application's Jar.
private static void addLocalesFromJar(List<Locale> locales) throws IOException {
ProtectionDomain domain = I18n.class.getProtectionDomain();
CodeSource src = domain.getCodeSource();
URL url = src.getLocation();
JarInputStream jar = new JarInputStream(url.openStream());
while (true) {
JarEntry entry = jar.getNextJarEntry();
if (entry == null) {
break;
}
String name = entry.getName();
// ...
}
}
Currently, this isn't working - jar.getNextJarEntry() seems to always return null. I have no idea why that's happening, all I know is that url is set to rsrc:./. I have never seen that protocol, and couldn't find anything about it.
Curiously, this works:
class Main {
public static void main(String[] args) {
URL url = Main.class.getProtectionDomain().getCodeSource().getLocation();
JarInputStream jar = new JarInputStream(url.openStream());
while (true) {
JarEntry entry = jar.getNextJarEntry();
if (entry == null) {
break;
}
System.out.println(entry.getName());
}
}
}
In this version, even though there is practically no difference between them, the url is correctly set to the path of the Jar file.
Why doesn't the first version work, and what is breaking it?
UPDATE:
The working example really only works if I don't use Eclipse to export it. It worked just fine in NetBeans, but in the Eclipse version the URL got set to rsrc:./ too.
Since I exported it with Package required libraries into generated JAR library handling, Eclipse put its jarinjarloader in my Jar so I can have all dependencies inside it. It works fine with the other settings, but is there any way to make this work independently of them?
Another question
At the moment, that class is part of my application, but I plan to put it in a separate library. In that case, how can I make sure it will work with separate Jars?
The problem is the jarinjarloader ClassLoader that is being used by Eclipse. Apparently it is using its own custom rsrc: URL scheme to point to jar files stored inside the main jar file. This scheme is not understood by your URL stream handler factory, so the openStream() method returns null which causes the problem that you're seeing.
This answers the second part of your question about separate jars - not only will this work, it's the only way that it will work. You need to change your main application to use separate jars instead of bundling them all up inside the main jar. If you're building a web application, copy them into the WEB-INF/lib directory and you're fine. If you're building a desktop application, add a relative path reference in the META-INF/MANIFEST.MF to the other jars, and they will automatically be included as part of the classpath when you run the main jar.
The code may or may not result into the jar file where I18n resides. Also getProtectionDomain can be null. It depends how the classloader is implemented.
ProtectionDomain domain = I18n.class.getProtectionDomain();
CodeSource src = domain.getCodeSource();
URL url = src.getLocation();
about the rsrc:./ protocol, the classloader is free to use whatever URL they please (or name it for that matter)
try this out, you might get lucky :)
URL url = getClass().getResource(getClass().getSimpleName()+".class");
java.net.JarURLConnection conn = (java.net.JarURLConnection) url.openConnection();
Enumeration<JarEntry> e = conn.getJarFile().entries();
...
and good luck!
Eclipse's jarinjarloader loads everything using the system classloader and it never knows what jar file it was loaded from. That's why you can't get the jar URL for a rsrc: url.
I suggest storing the list of locales in a file in each application jar, e.g. META-INF/locales. Then you can use ClassLoader.getResources("META-INF/locales") to get the list of all the files with that name in the classpath and combine them to obtain the full list of locales.
I use System.getProperty("java.class.path") for getting the location of the jar. I do not know if that makes a difference. I have not explored the ProtectDomain path so I cannot help you there, sorry. As for multiple jars, just iterate through those jar file also.

How to walk through Java class resources?

I know we can do something like this:
Class.class.getResourceAsStream("/com/youcompany/yourapp/module/someresource.conf")
to read the files that are packaged within our jar file.
I have googled it a lot and I am surely not using the proper terms; what I want to do is to list the available resources, something like this:
Class.class.listResources("/com/yourcompany/yourapp")
That should return a list of resources that are inside the package com.yourcompany.yourapp.*
Is that possible? Any ideas on how to do it in case it can't be done as easily as I showed?
Note: I know it is possible to know where your jar is and then open it and inspect its contents to achieve it. But, I can't do it in the environment I am working in now.
For resources in a JAR file, something like this works:
URL url = MyClass.class.getResource("MyClass.class");
String scheme = url.getProtocol();
if (!"jar".equals(scheme))
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
JarURLConnection con = (JarURLConnection) url.openConnection();
JarFile archive = con.getJarFile();
/* Search for the entries you care about. */
Enumeration<JarEntry> entries = archive.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith("com/y/app/")) {
...
}
}
You can do the same thing with resources "exploded" on the file system, or in many other repositories, but it's not quite as easy. You need specific code for each URL scheme you want to support.
In general can't get a list of resources like this. Some classloaders may not even be able to support this - imagine a classloader which can fetch individual files from a web server, but the web server doesn't have to support listing the contents of a directory.
For a jar file you can load the contents of the jar file explicitly, of course.
(This question is similar, btw.)
The most robust mechanism for listing all resources in the classpath is currently to use this pattern with ClassGraph, because it handles the widest possible array of classpath specification mechanisms, including the new JPMS module system. (I am the author of ClassGraph.)
List<String> resourceNames;
try (ScanResult scanResult = new ClassGraph()
.whitelistPaths("com/yourcompany/yourapp")
.scan()) {
resourceNames = scanResult.getAllResources().getNames();
}
I've been looking for a way to list the contents of a jar file using the classloaders, but unfortunately this seems to be impossible. Instead what you can do is open the jar as a zip file and get the contents this way. You can use standard (here) ways to read the contents of a jar file and then use the classloader to read the contents.
I usually use
getClass().getClassLoader().getResourceAsStream(...)
but I doubt you can list the entries from the classpath, without knowing them a priori.

Categories

Resources