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

前端 未结 22 2307
小蘑菇
小蘑菇 2020-11-22 10:30

Given two absolute paths, e.g.

/var/data/stuff/xyz.dat
/var/data

How can one create a relative path that uses the second path as its base?

22条回答
  •  难免孤独
    2020-11-22 10:48

    Matt B's solution gets the number of directories to backtrack wrong -- it should be the length of the base path minus the number of common path elements, minus one (for the last path element, which is either a filename or a trailing "" generated by split). It happens to work with /a/b/c/ and /a/x/y/, but replace the arguments with /m/n/o/a/b/c/ and /m/n/o/a/x/y/ and you will see the problem.

    Also, it needs an else break inside the first for loop, or it will mishandle paths that happen to have matching directory names, such as /a/b/c/d/ and /x/y/c/z -- the c is in the same slot in both arrays, but is not an actual match.

    All these solutions lack the ability to handle paths that cannot be relativized to one another because they have incompatible roots, such as C:\foo\bar and D:\baz\quux. Probably only an issue on Windows, but worth noting.

    I spent far longer on this than I intended, but that's okay. I actually needed this for work, so thank you to everyone who has chimed in, and I'm sure there will be corrections to this version too!

    public static String getRelativePath(String targetPath, String basePath, 
            String pathSeparator) {
    
        //  We need the -1 argument to split to make sure we get a trailing 
        //  "" token if the base ends in the path separator and is therefore
        //  a directory. We require directory paths to end in the path
        //  separator -- otherwise they are indistinguishable from files.
        String[] base = basePath.split(Pattern.quote(pathSeparator), -1);
        String[] target = targetPath.split(Pattern.quote(pathSeparator), 0);
    
        //  First get all the common elements. Store them as a string,
        //  and also count how many of them there are. 
        String common = "";
        int commonIndex = 0;
        for (int i = 0; i < target.length && i < base.length; i++) {
            if (target[i].equals(base[i])) {
                common += target[i] + pathSeparator;
                commonIndex++;
            }
            else break;
        }
    
        if (commonIndex == 0)
        {
            //  Whoops -- not even a single common path element. This most
            //  likely indicates differing drive letters, like C: and D:. 
            //  These paths cannot be relativized. Return the target path.
            return targetPath;
            //  This should never happen when all absolute paths
            //  begin with / as in *nix. 
        }
    
        String relative = "";
        if (base.length == commonIndex) {
            //  Comment this out if you prefer that a relative path not start with ./
            //relative = "." + pathSeparator;
        }
        else {
            int numDirsUp = base.length - commonIndex - 1;
            //  The number of directories we have to backtrack is the length of 
            //  the base path MINUS the number of common path elements, minus
            //  one because the last element in the path isn't a directory.
            for (int i = 1; i <= (numDirsUp); i++) {
                relative += ".." + pathSeparator;
            }
        }
        relative += targetPath.substring(common.length());
    
        return relative;
    }
    

    And here are tests to cover several cases:

    public void testGetRelativePathsUnixy() 
    {        
        assertEquals("stuff/xyz.dat", FileUtils.getRelativePath(
                "/var/data/stuff/xyz.dat", "/var/data/", "/"));
        assertEquals("../../b/c", FileUtils.getRelativePath(
                "/a/b/c", "/a/x/y/", "/"));
        assertEquals("../../b/c", FileUtils.getRelativePath(
                "/m/n/o/a/b/c", "/m/n/o/a/x/y/", "/"));
    }
    
    public void testGetRelativePathFileToFile() 
    {
        String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
        String base = "C:\\Windows\\Speech\\Common\\sapisvr.exe";
    
        String relPath = FileUtils.getRelativePath(target, base, "\\");
        assertEquals("..\\..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
    }
    
    public void testGetRelativePathDirectoryToFile() 
    {
        String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
        String base = "C:\\Windows\\Speech\\Common";
    
        String relPath = FileUtils.getRelativePath(target, base, "\\");
        assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
    }
    
    public void testGetRelativePathDifferentDriveLetters() 
    {
        String target = "D:\\sources\\recovery\\RecEnv.exe";
        String base   = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\";
    
        //  Should just return the target path because of the incompatible roots.
        String relPath = FileUtils.getRelativePath(target, base, "\\");
        assertEquals(target, relPath);
    }
    

提交回复
热议问题