3

I'm creating a command line application that needs to output some files (more than one) into either a ZIP file or a plain folder depending on the paramters given.

My approach is to encapsulate the target (plain folder/ZIP file) with a FileSystem.

My problem is that I cannot sucessfully create a FileSystem object for a directory other than the current working directory denoting an absolute path on my hard disk:

public class FileSystemWriteTest {
    public static void main(String[] args) throws IOException {
        Path absolutePath = Paths.get("target", "testpath").toAbsolutePath();
        System.out.println(String.format("user.dir before change:\n %s", System.getProperty("user.dir")));


        System.setProperty("user.dir", absolutePath.toString());
        System.out.println(String.format("changed user.dir:\n %s", System.getProperty("user.dir")));
        FileSystem defaultSystem = FileSystems.getDefault();
        Path testFilePath = defaultSystem.getPath("test.file");
        System.out.println(String.format("expected to be in changed user.dir:\n %s", testFilePath.toAbsolutePath()));


        URI uri = absolutePath.toUri();
        System.out.println(String.format("URI: %s", uri));
        FileSystem localFileSystem =
                FileSystems.newFileSystem(uri, Collections.emptyMap());
        Path file = localFileSystem.getPath("test.txt");
        System.out.println(file.toAbsolutePath());
    }
}

The output is:

user.dir before change:
 D:\data\scm-workspace\anderes\Test
changed user.dir:
 D:\data\scm-workspace\anderes\Test\target\testpath
expected to be in changed user.dir:
 D:\data\scm-workspace\anderes\Test\test.file
URI: file:///D:/data/scm-workspace/anderes/Test/target/testpath/
Exception in thread "main" java.lang.IllegalArgumentException: Path component should be '/'
    at sun.nio.fs.WindowsFileSystemProvider.checkUri(Unknown Source)
    at sun.nio.fs.WindowsFileSystemProvider.newFileSystem(Unknown Source)
    at java.nio.file.FileSystems.newFileSystem(Unknown Source)
    at java.nio.file.FileSystems.newFileSystem(Unknown Source)
    at com.oc.test.filesystem.FileSystemWriteTest.main(FileSystemWriteTest.java:27)

If I change to FileSystems.newFileSystem(Path, Classloader) the Exception changes to:

Exception in thread "main" java.nio.file.ProviderNotFoundException: Provider not found
    at java.nio.file.FileSystems.newFileSystem(Unknown Source)
    at com.oc.test.filesystem.FileSystemWriteTest.main(FileSystemWriteTest.java:27)

Looks like this only works with regular files, not with directories.

So how can I create a FileSystem object for a directory other than pwd?

InsaneCat
  • 2,115
  • 5
  • 21
  • 40
Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51
  • Given your requirement (first sentence) I don't think `java.nio.file.FileSystem` is a useful approach. Why not use `java.io.File`and `java.util.zip.ZipFile`? – Würgspaß Sep 03 '18 at 11:24
  • @Würgspaß Because there a different ways to create a new "file". I don't want the Code actually createing the files needing to know what target is used. (all the fliles should go into the same ZIP or Directory) – Timothy Truckle Sep 03 '18 at 11:51
  • Creating a new file system requires an appropriate file system provider implementation. There is no such directory encapsulation file system shipped with the JDK. – Holger Sep 03 '18 at 15:23
  • @Holger then how does `FileSystems.getDefault()` work which returns the *pwd* as root? – Timothy Truckle Sep 03 '18 at 16:29
  • `FileSystems.getDefault()` does *not* use the pwd as root. The root directories are what you get when calling `FileSystems.getDefault().getRootDirectories()`. What you did, was just resolving *relative paths*, which always happens against the current directory. This doesn’t make it a root. You still can resolve relating paths like `..` or, in case of Windows, `\ `. In that regard, there is no difference between `FileSystems.getDefault().getPath("test.file")` and `Paths.get("test.file")`. – Holger Sep 04 '18 at 08:40
  • @Holger *"What you did, was just resolving relative paths, which always happens against the current directory."* But that is my requirement: I need to resolve relative Paths (more precise file names without paths) starting at a different place than *pwd*. – Timothy Truckle Sep 04 '18 at 08:45
  • But do you really need `someFileSystem.getPath("relative/path")` instead of `someAbsolutePath.resolve("relative/path")`? – Holger Sep 04 '18 at 11:40
  • @Holger yes. I want the user just calling `someFileSystem.getPath("file")` not needing to know if that will crete the file in the root of the ZIP or at some random place on the hard disk given at the command line. Looks like I Need to write my own abstaction althogh `FileSystem` should do the trick.... :o( – Timothy Truckle Sep 04 '18 at 11:46
  • Whether you pass a `FileSystem` or a root `Path` instance, where’s the difference? In either case, you are passing some object, which the receiver shall use to resolve relative paths, without the need to know, whether it belongs to the default filesystem, a zip file, or whatever. – Holger Sep 04 '18 at 11:51
  • @Holger OK, using a `Path` object as the abstraction model works well. Thank you. Would you mind to add this as answer I can accept? – Timothy Truckle Sep 04 '18 at 12:05

1 Answers1

3

There is no built facility for creating a FileSystem with chroot like semantics. The default file system only supports file:/// as URI and doesn’t allow more than one instantiation.

In this regard, FileSystems.getDefault().getPath("test.file") creates a relative path, just like Paths.get("test.file"). The difference between relative paths created for the default file system and other file systems lies in the resolve behavior when no other base path has been specified (e.g. when calling toAbsolutePath() or just trying to open them). But being resolved against the current working directory does not make them a root path.

The best solution for implementing file system agnostic operations, is to let the code receive base Path objects, to resolve relative paths against.

E.g. a simple tree copy routine may look like:

static void copyTree(Path sourceBase, Path targetBase) throws IOException {
    try {
        Files.walk(sourceBase).forEach(path -> {
            if(Files.isRegularFile(path)) try {
                Path target = targetBase.resolve(sourceBase.relativize(path).toString());
                if(!Files.isDirectory(target.getParent()))
                    Files.createDirectories(target.getParent());
                Files.copy(path, target, StandardCopyOption.COPY_ATTRIBUTES);
            } catch(IOException ex) {
                throw new UncheckedIOException(ex);
            }
        });
    } catch(UncheckedIOException ex) {
        throw ex.getCause();
    }
}

For this method, it doesn’t matter whether you’re copying from a harddrive directory to another harddrive directory or to a zip filesystem, or from a zip filesystem to the harddrive, or from a zip file to another zip file, etc.

The most interesting part is the invocation of sourceBase.relativize(path), to get the relative path from the source base path to the sub-path of the actual file. Since relative Path instances still are tied to a particular filesystem, the code invokes toString(), before passing it to targetBase.resolve(…), to ensure that it will work across different filesystems. Note that path.resolve(string) is equivalent to path.resolve(path.getFileSystem().getPath(string)). It would be legitimate, if the method first checks whether both Path instances belong to the same filesystem, to skip the String detour in that case.

Holger
  • 285,553
  • 42
  • 434
  • 765