I found that a lot of solutions only work when a) executing from a jar or b) executing in a development environment, but none seem to support both scenarios. Additionally, I had a scenario where some resources were located within my project while others were located in external jars (such as dependencies).
This is this the solution I created to address these scenarios:
public class ResourceWalker { private Path rootPath; private boolean inJar; private Class<?> context; public ResourceWalker(Class<?> context) throws IOException { if (context.isAnonymousClass()) throw new IllegalArgumentException("Context cannot be an anonymous class"); if (context.isLocalClass()) throw new IllegalArgumentException("Context cannot be a local class"); if (context.isSynthetic()) throw new IllegalArgumentException("Context cannot be synthetic"); this.context = context; URL contextUrl = context.getResource('/' + context.getName().replace('.', '/') + ".class"); if (this.inJar = contextUrl.getProtocol().equals("jar")) { URLConnection connection = contextUrl.openConnection(); if (!(connection instanceof JarURLConnection)) throw new RuntimeException("Cannot determine jar for context: " + contextUrl); this.rootPath = Paths.get(((JarURLConnection) connection).getJarFile().getName()); } else { try { this.rootPath = Paths.get(context.getProtectionDomain().getCodeSource().getLocation().toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } } } public void walkTree(String resourcePath, FileVisitor<String> visitor) throws IOException { if (this.inJar) { try (FileSystem system = FileSystems.newFileSystem(this.rootPath, null)) { Path path = system.getPath(resourcePath); Files.walkFileTree(path, new ResourceVisitorAdapter(visitor)); } } else if (resourcePath.equals("/")) { this.walk(resourcePath, resourcePath, visitor); } else { this.walk(resourcePath, resourcePath + "/", visitor); } } private FileVisitResult walk(String path, String base, FileVisitor<String> visitor) throws IOException { FileVisitResult result = visitor.preVisitDirectory(path, null); switch (result) { case SKIP_SUBTREE: return FileVisitResult.CONTINUE; case SKIP_SIBLINGS: case TERMINATE: return result; default: break; } try (BufferedReader reader = new BufferedReader( new InputStreamReader(this.context.getResourceAsStream(path)))) { String entry; while ((entry = reader.readLine()) != null) { entry = base + entry; if (this.isDirectory(entry)) result = this.walk(entry, entry + "/", visitor); else result = visitor.visitFile(entry, null); switch (result) { case SKIP_SIBLINGS: case TERMINATE: return result; default: break; } } } return visitor.postVisitDirectory(path, null); } private boolean isDirectory(String entry) { if (entry.charAt(0) == '/') entry = entry.substring(1); return Files.isDirectory(this.rootPath.resolve(entry)); } private static class ResourceVisitorAdapter implements FileVisitor<Path> { private FileVisitor<String> delegate; private ResourceVisitorAdapter(FileVisitor<String> delegate) { this.delegate = delegate; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return this.delegate.preVisitDirectory(dir.toString(), attrs); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { return this.delegate.visitFile(file.toString(), attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return this.delegate.visitFileFailed(file.toString(), exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return this.delegate.postVisitDirectory(dir.toString(), exc); } } }
You'll notice that the constructor takes a class parameter. This is to help the scanner identify where the target resources live. For example, if I have an external jar emojis.jar which contains
- A resource folder:
/images/emojis - A class:
com.goof.emojis.Emojis
I can scan the contents of the emojis folder by providing Emojis.class as the "context" class:
FileVisitor<String> visitor = ...; ResourceWalker walker = new ResourceWalker(com.goof.emojis.Emojis.class); walker.walkTree("/images/emojis", visitor);
I wanted to comment on this line:
URL contextUrl = context.getResource('/' + context.getName().replace('.', '/') + ".class");
You may be wondering why I am using this approach to locate the jar's disk location when I could just use ProtectionDomain#getCodeSource. I learned the hard way that classes loaded with the bootstrap class loader will always return the same CodeSource, even if they originated from different locations. My workaround above was inspired by this thread: Determine which JAR file a class is from.
Hopefully this helps.