Home > database >  Copy directory from a jar file using only pure java
Copy directory from a jar file using only pure java

Time:12-18

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"));
  • Related