Inside my resources folder I have a folder called init. I want to copy that folder and everything inside of it to the outside of the jar in a folder called ready. And I want to do that without using any external libraries, just pure java.
I have tried the following
public static void copyFromJar(String source, final Path target)
throws
URISyntaxException,
IOException
{
URI resource = ServerInitializer.class.getResource("").toURI();
FileSystem fileSystem = FileSystems.newFileSystem(resource, Collections.<String, String>emptyMap());
final Path jarPath = fileSystem.getPath(source);
Files.walkFileTree(jarPath, new SimpleFileVisitor<>()
{
private Path currentTarget;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws
IOException
{
currentTarget = target.resolve(jarPath.relativize(dir).toString());
Files.createDirectories(currentTarget);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws
IOException
{
Files.copy(file, target.resolve(jarPath.relativize(file).toString()),
StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
However my application already dies at line
FileSystem fileSystem = FileSystems.newFileSystem(resource, Collections.<String, String>emptyMap());
with exception
java.lang.IllegalArgumentException: Path component should be '/'
when I call
copyFromJar("/init", Paths.get("ready");
Any idea what I am doing wrong? Or can someone provide me code to copy directory from jar to outside of it without using any external libraries?
Just for reference I already looked at this solution but it is too old and uses apache library but I need pure java solution that works both on windows and linux.
CodePudding user response:
That's a crazy complicated way to do it. It's also dependent entirely on your app being in a jar, which makes testing, deployments into runtime-modularized loaders, etc - tricky.
There are much easier ways to do this. Specifically, SomeClass.class.getResourceAsStream("/foo/bar")
will get you an InputStream
for the entry /foo/bar
in the same classpath root as where the class file representing SomeClass
lives - be it a jar file, a plain old directory, or something more exotic.
That's how you should 'copy' your files over. This trivial code:
String theResource = "com/foo/quillionsapp/toCopy/example.txt";
Path targetPath = ....;
try (var in = YourClass.class.getResourceAsStream("/" theResource)) {
Path tgt = targetPath.resolve(theResource);
Files.createDirectories(tgt.getParent());
try (var out = Files.newOutputStream(tgt)) {
in.transferTo(out);
}
}
Now all you need is a list of all files to be copied. The classpath abstraction simply does not support listing. So, any attempt to hack that in there is just that: A hack. It'll fail when you e.g. have modularized loaders and the like. You can just do that - you're already writing code that asplodes on you if your code is not in a jar file. It's not hard to write a method that gives you a list of all contents for both 'dir on the file system' based classpaths as well as 'jar file' based ones, but there is a ready alternative: Make a text file with a known name that lists all resources at compile time. You can write it yourself, or you can script it. Can be as trivial as ls src/resources/* > filesToCopy.txt
or whatnot. You can also use annotation processors to produce such a file.
Once you know the file exists (it's in the jar same as the files you want to copy), read it with getResourceAsStream
just the same way, and now you have a list of resources to write out using the above code. That trick means your code is entirely compatible with the API of ClassLoader: You are just relying on the guaranteed functionality of 'get me an inputstream with the full data of this named resource' and nothing else.
CodePudding user response:
Take note of the answer and warnings by @rzwitserloot, I also would not recommend this approach as it will not run in all circumstances - only from specific jars not exploded filesystems so therefore would not work via IDE debuggers, and might not work in the future.
Having said that, all you are doing is unzip from a ZIP filesystem. That requires a simple helper method to copy
any resource to target:
static boolean copy(Path from, BasicFileAttributes a, Path target) {
System.out.println("Copy " (a.isDirectory() ? "DIR " : "FILE") " => " target);
try {
if (a.isDirectory())
Files.createDirectories(target);
else if (a.isRegularFile())
Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
return true;
}
The difference from unzip
is to create filesystem from the classLoader not Path
.
static void copyDir(ClassLoader classLoader, String resPath, Path target) throws IOException, URISyntaxException {
System.out.println("copyDir(" resPath ", " target ")");
URI uri = classLoader.getResource(resPath).toURI();
BiPredicate<Path, BasicFileAttributes> foreach = (p,a) -> copy(p,a, Path.of(target.toString(), p.toString())) && false;
try(var fs = FileSystems.newFileSystem(uri, Map.of())) {
final Path subdir = fs.getPath(resPath);
for (Path root : fs.getRootDirectories()) {
System.out.println("root: " root);
try (Stream<Path> stream = Files.find(subdir, Integer.MAX_VALUE, foreach)) {
stream.count();
}
}
}
}
Then you can call with a suitable classloader and paths:
copyDir(yourapp.getClass().getClassLoader(), "some/path/in/jar", Path.of("copied.resource.dir"));