3

Context: My software depends on calling a library which can only accept relative paths as an input because of an old limitation. I need the paths to be relative to a known directory. The library might make a call internally like

java.io.File fooBar = new java.io.File("foo/bar");

I need this to give me /nwd/foo/bar and not, say, /cwd/foo/bar where /cwd is the working directory from which java was run.

For all intents and purposes, I cannot modify the internal behavior of this library. Manually overriding the methods which instantiate these objects would involve basically rewriting the entire library.

A tempting solution would be to just System.setProperty("user.dir", "/nwd") before calling the library, but this doesn't actually give me the desired effect. Indeed, if I called fooBar.getAbsolutePath(), I would get the desired /nwd/foo/bar, but if I checked fooBar.exists() or tried to open the file for reading or writing, it would appear that the file doesn't exist, because it's actually trying to open /cwd/foo/bar. In fact, if fooBar were instead initialized by

java.io.File fooBar = new java.io.File(new java.io.File("foo/bar").getAbsolutePath());

that would actually work, because then the File object actually contains absolute references.

At this point, I'm so frustrated that I don't care if this requires a hacky solution. I just need the effect of changing the working directory.

2mac
  • 1,609
  • 5
  • 20
  • 35
  • You would have only tricky solutions according to your constraints. Why not move the files loaded by the library with in the base directory where the jvm is launched ? – davidxxx Apr 26 '18 at 19:29
  • @davidxxx Because there are multiple such directories that a single instance of the program might need to touch. The program needs to be working directory agnostic. – 2mac Apr 26 '18 at 19:30
  • `../nwd/foo/bar`? Note that `user.dir` and most other properties like that are read only (fortunately, you don't want to have a fight between multiple components of your application setting the working folder, right?) – Maarten Bodewes Apr 26 '18 at 21:17
  • When starting your program, can't you start your program from the correct working directory for the library? – Ferrybig May 04 '18 at 07:21
  • @Ferrybig No, because there are multiple such directories for any given session of the application. – 2mac May 04 '18 at 16:12

4 Answers4

1

A very alternative way to change the CWD of the library would be to launch it in a different Java process, for which you can specify CWD at launch time (see for example ProcessBuilder documentation). If I understand the problem correctly, your current flow is somewhat similar to

 launch program with CWD 'a'
 use library X that expects CWD to be 'b' // <-- problem here

The new flow would be

 launch program with CWD 'a'
 determine desired CWD for launching library X
 launch wrapper for library X with CWD 'b'
    internally, library X is happy because its CWD is as expected

Of course, this will force you to write a full wrapper, and communicate it using sockets&serialization, or any other communication strategy of your choice. On the plus side, this will allow you to launch several instances of your library side-by-side, without their CWDs interfering with each other -- at the cost of JVM and communication overheads.

tucuxi
  • 17,561
  • 2
  • 43
  • 74
0

I don't think this is a big issue, because a relative path can start from root as well.

So let assume your current directory is /user/home and you want to refer to /user/tarun/foo/bar then you would give the relative path to be ../tarun/foo/bar to your library.

And for that you can use the code as discussed in below SO thread

How to construct a relative path in Java from two absolute paths (or URLs)?

String path = "/var/data/stuff/xyz.dat";
String base = "/var/data";
String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath();
Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
  • The problem here is that it's the library that comes up with the relative paths while assuming it's in the correct working directory. I don't have a way to tell the library "use this as your base directory", which is the reason I'm seeking to get the effect of changing the working directory. – 2mac May 03 '18 at 20:59
  • Would need to look into Java source why `user.dir` is not working. Will check and get back later – Tarun Lalwani May 04 '18 at 07:09
  • So it seems it goes to Native JVM for the methods for things like exists. `exists` is in Java but it use some method internally which is native and not in Java code. And the way to use it is to use `getAbsoluteFile` and then all will work, Now you need to somehow see how you can patch the library that you use for it to work – Tarun Lalwani May 04 '18 at 08:57
0

A big big big disclaimer here! Don't do this!!!!

You aren't supposed to be able to set the root directory once it is set. I'm not exactly sure what the design philosophy is here, but I imagine it's to do with security and not allowing malicious code to get your application to do something unexpected. That said, there are ways we can get around this using some nasty reflection.

I achieved this hack by debugging where the Paths class finds it's default directory and loads files from. I use reflection to edit the values of the FileSystem class which I saw that it uses. This hack is going to be operating system dependent as it edits the FileSystem instance. It might also need to be tweaked as there's no guarantee that the instance wont be renewed. You'll have to make sure to test it on all the target systems as they will have different FileSystem implementations (BUT PLEASE DON'T ACTUALLY DO THIS).

Further with the new changes in Java 9 you wont be able to make this hack work easily (they disallow using reflection to edit classes not exposed by the "module"). Your best bet is to spin up a new JVM instance and make the call there with the -Droot property passed in. It's not a pretty solution but neither is the code-base you're working with. You should actively attempt to fix this in a more sensible manner. (Maybe a re-write of the library is the best way to go if the business function is actually important)

Working demo with a file in my working directory and a file in my C:\\:

public static void main(final String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);

    final Class windowsFileSystemClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsFileSystem");
    final Field windowsFileSystemClassField = windowsFileSystemClass.getDeclaredField("defaultDirectory");
    windowsFileSystemClassField.setAccessible(true);

    final Class windowsPathClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsPath");
    final Field windowsPathFsField = windowsPathClass.getDeclaredField("fs");
    windowsPathFsField.setAccessible(true);

    // Hack that uses a path instance to grab reference to the shared FileSystem instance.
    final Object fileSystem = windowsPathFsField.get(Paths.get(""));
    windowsFileSystemClassField.set(fileSystem, "C:\\");

    Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);
}

This outputs:

I'm in the working directory
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.Main (file:/C:/Users/calve/IdeaProjects/untitled1/out/production/untitled1/) to field sun.nio.fs.WindowsFileSystem.defaultDirectory
WARNING: Please consider reporting this to the maintainers of com.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
I'm in C: now

Please note and heed the warnings the JVM just produced.

flakes
  • 21,558
  • 8
  • 41
  • 88
  • I really like your approach, description and disclaimers - but I cannot, in good faith, up-vote as an answer to OP's problem, because as you correctly point out it is ugly as hell. There should be an "kudos to poster" button. – tucuxi May 04 '18 at 10:42
  • 1
    @tucuxi thanks! I added the warnings as a caution to future readers, but I like to also provide an answer to the question as objectively as I can. There may be a complex use case for somebody somewhere where this is the lesser of some evil. It's our job to provide information on what's possible, not gatekeep knowledge. We have to trust the readers to make informed decisions for themselves. – flakes May 04 '18 at 18:07
  • I'm awarding you the bounty since you gave the most direct and "correct" answer to the question, but I think I'm going to go with a variation of @tucuxi's answer by just multiprocessing. I just wanted to avoid that route. – 2mac May 04 '18 at 19:39
0

why not copy the file from /nwd/foo/bar to /cwd/foo/bar then you can use your existing library as it is :)

rohit thomas
  • 2,302
  • 11
  • 23
  • Because `/cwd` might contain files with the same name as those in `/nwd` which would be overwritten. – 2mac May 04 '18 at 19:42