javajarjava-11getresource

Copy directory from a jar file using only pure java


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.


Solution

  • 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.