给出两条绝对路径,例如
/var/data/stuff/xyz.dat /var/data
如何创建使用第二条路径作为基础的相对路径?在上面的例子中,结果应该是:./stuff/xyz.dat
这是一个小回旋,但为什么不使用URI?它有一个相对化的方法,为您做所有必要的检查.
String path = "/var/data/stuff/xyz.dat"; String base = "/var/data"; String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath(); // relative == "stuff/xyz.dat"
请注意,对于文件路径,java.nio.file.Path#relativize
自Java 1.7开始,正如@Jirka Meluzin在另一个答案中指出的那样.
从Java 7开始,您可以使用relativize方法:
import java.nio.file.Path; import java.nio.file.Paths; public class Test { public static void main(String[] args) { Path pathAbsolute = Paths.get("/var/data/stuff/xyz.dat"); Path pathBase = Paths.get("/var/data"); Path pathRelative = pathBase.relativize(pathAbsolute); System.out.println(pathRelative); } }
输出:
stuff/xyz.dat
在撰写本文时(2010年6月),这是通过我的测试用例的唯一解决方案.我不能保证这个解决方案没有错误,但它确实通过了包含的测试用例.我编写的方法和测试依赖于Apache commons IO的FilenameUtils
类.
该解决方案使用Java 1.4进行测试.如果您使用的是Java 1.5(或更高版本),你应该考虑更换StringBuffer
用StringBuilder
(如果你还在使用Java 1.4,你应该考虑雇主的变化,而不是).
import java.io.File; import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; public class ResourceUtils { /** * Get the relative path from one file to another, specifying the directory separator. * If one of the provided resources does not exist, it is assumed to be a file unless it ends with '/' or * '\'. * * @param targetPath targetPath is calculated to this file * @param basePath basePath is calculated from this file * @param pathSeparator directory separator. The platform default is not assumed so that we can test Unix behaviour when running on Windows (for example) * @return */ public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { // Normalize the paths String normalizedTargetPath = FilenameUtils.normalizeNoEndSeparator(targetPath); String normalizedBasePath = FilenameUtils.normalizeNoEndSeparator(basePath); // Undo the changes to the separators made by normalization if (pathSeparator.equals("/")) { normalizedTargetPath = FilenameUtils.separatorsToUnix(normalizedTargetPath); normalizedBasePath = FilenameUtils.separatorsToUnix(normalizedBasePath); } else if (pathSeparator.equals("\\")) { normalizedTargetPath = FilenameUtils.separatorsToWindows(normalizedTargetPath); normalizedBasePath = FilenameUtils.separatorsToWindows(normalizedBasePath); } else { throw new IllegalArgumentException("Unrecognised dir separator '" + pathSeparator + "'"); } String[] base = normalizedBasePath.split(Pattern.quote(pathSeparator)); String[] target = normalizedTargetPath.split(Pattern.quote(pathSeparator)); // First get all the common elements. Store them as a string, // and also count how many of them there are. StringBuffer common = new StringBuffer(); int commonIndex = 0; while (commonIndex < target.length && commonIndex < base.length && target[commonIndex].equals(base[commonIndex])) { common.append(target[commonIndex] + pathSeparator); commonIndex++; } if (commonIndex == 0) { // No single common path element. This most // likely indicates differing drive letters, like C: and D:. // These paths cannot be relativized. throw new PathResolutionException("No common path element found for '" + normalizedTargetPath + "' and '" + normalizedBasePath + "'"); } // The number of directories we have to backtrack depends on whether the base is a file or a dir // For example, the relative path from // // /foo/bar/baz/gg/ff to /foo/bar/baz // // ".." if ff is a file // "../.." if ff is a directory // // The following is a heuristic to figure out if the base refers to a file or dir. It's not perfect, because // the resource referred to by this path may not actually exist, but it's the best I can do boolean baseIsFile = true; File baseResource = new File(normalizedBasePath); if (baseResource.exists()) { baseIsFile = baseResource.isFile(); } else if (basePath.endsWith(pathSeparator)) { baseIsFile = false; } StringBuffer relative = new StringBuffer(); if (base.length != commonIndex) { int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex; for (int i = 0; i < numDirsUp; i++) { relative.append(".." + pathSeparator); } } relative.append(normalizedTargetPath.substring(common.length())); return relative.toString(); } static class PathResolutionException extends RuntimeException { PathResolutionException(String msg) { super(msg); } } }
这个测试用例是
public void testGetRelativePathsUnix() { assertEquals("stuff/xyz.dat", ResourceUtils.getRelativePath("/var/data/stuff/xyz.dat", "/var/data/", "/")); assertEquals("../../b/c", ResourceUtils.getRelativePath("/a/b/c", "/a/x/y/", "/")); assertEquals("../../b/c", ResourceUtils.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 = ResourceUtils.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 = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath); } public void testGetRelativePathFileToDirectory() { String target = "C:\\Windows\\Boot\\Fonts"; String base = "C:\\Windows\\Speech\\Common\\foo.txt"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts", relPath); } public void testGetRelativePathDirectoryToDirectory() { String target = "C:\\Windows\\Boot\\"; String base = "C:\\Windows\\Speech\\Common\\"; String expected = "..\\..\\Boot"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals(expected, relPath); } public void testGetRelativePathDifferentDriveLetters() { String target = "D:\\sources\\recovery\\RecEnv.exe"; String base = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\"; try { ResourceUtils.getRelativePath(target, base, "\\"); fail(); } catch (PathResolutionException ex) { // expected exception } }
使用java.net.URI.relativize时你应该知道Java bug: JDK-6226081(URI应该能够使具有部分根的路径相对化)
目前,只有当一个是另一个的前缀时,该
relativize()
方法URI
才会重新激活URI.
这基本上意味着java.net.URI.relativize
不会为你创造"..".
在@Peter穆勒的回答中提到的错误被解决URIUtils在Apache的HttpComponents
public static URI resolve(URI baseURI, String reference)
解析针对基URI的URI引用.解决java.net.URI()中的错误
在Java 8中,您可以轻松完成(相比之下URI
,它是无bug的):
Path#relativize(Path)
如果您知道第二个字符串是第一个字符串的一部分:
String s1 = "/var/data/stuff/xyz.dat"; String s2 = "/var/data"; String s3 = s1.substring(s2.length());
或者如果你真的想要在你的例子中开头的句号:
String s3 = ".".concat(s1.substring(s2.length()));
递归产生较小的解决方案.如果结果不可能(例如,不同的Windows磁盘)或不切实际(root只是公共目录),则抛出异常.
/** * Computes the path for a file relative to a given base, or fails if the only shared * directory is the root and the absolute form is better. * * @param base File that is the base for the result * @param name File to be "relativized" * @return the relative name * @throws IOException if files have no common sub-directories, i.e. at best share the * root prefix "/" or "C:\" */ public static String getRelativePath(File base, File name) throws IOException { File parent = base.getParentFile(); if (parent == null) { throw new IOException("No common directory"); } String bpath = base.getCanonicalPath(); String fpath = name.getCanonicalPath(); if (fpath.startsWith(bpath)) { return fpath.substring(bpath.length() + 1); } else { return (".." + File.separator + getRelativePath(parent, name)); } }
这是其他免费图书馆的解决方案:
Path sourceFile = Paths.get("some/common/path/example/a/b/c/f1.txt"); Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); Path relativePath = sourceFile.relativize(targetFile); System.out.println(relativePath);
输出
..\..\..\..\d\e\f2.txt
[编辑]实际上它输出更多.. \因为源文件不是目录.我的案例的正确解决方案是:
Path sourceFile = Paths.get(new File("some/common/path/example/a/b/c/f1.txt").parent()); Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); Path relativePath = sourceFile.relativize(targetFile); System.out.println(relativePath);
我的版本基于马特和史蒂夫的版本:
/** * Returns the path of one File relative to another. * * @param target the target directory * @param base the base directory * @return target's path relative to the base directory * @throws IOException if an error occurs while resolving the files' canonical names */ public static File getRelativeFile(File target, File base) throws IOException { String[] baseComponents = base.getCanonicalPath().split(Pattern.quote(File.separator)); String[] targetComponents = target.getCanonicalPath().split(Pattern.quote(File.separator)); // skip common components int index = 0; for (; index < targetComponents.length && index < baseComponents.length; ++index) { if (!targetComponents[index].equals(baseComponents[index])) break; } StringBuilder result = new StringBuilder(); if (index != baseComponents.length) { // backtrack to base directory for (int i = index; i < baseComponents.length; ++i) result.append(".." + File.separator); } for (; index < targetComponents.length; ++index) result.append(targetComponents[index] + File.separator); if (!target.getPath().endsWith("/") && !target.getPath().endsWith("\\")) { // remove final path separator result.delete(result.length() - File.separator.length(), result.length()); } return new File(result.toString()); }
Matt B的解决方案获取错误回溯的目录数 - 它应该是基本路径的长度减去公共路径元素的数量减去1(对于最后一个路径元素,可以是文件名,也可以是""
生成的尾随split
) .它发生一起工作/a/b/c/
和/a/x/y/
,但替换的参数/m/n/o/a/b/c/
和/m/n/o/a/x/y/
,你会看到这个问题.
此外,它需要else break
在第一个for循环内部,否则它将错误处理碰巧具有匹配目录名称的路径,例如/a/b/c/d/
和/x/y/c/z
- c
两个数组中的相同插槽中,但不是实际匹配.
所有这些解决方案都缺乏处理无法相互关联的路径的能力,因为它们具有不兼容的根,例如C:\foo\bar
和D:\baz\quux
.可能只是Windows上的一个问题,但值得注意.
我花的时间比我想的要长得多,但没关系.我实际上需要这个用于工作,所以感谢所有已经插入的人,我相信这个版本也会有更正!
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; }
这里有几个案例的测试:
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); }