Как построить относительный путь в Java из двух абсолютных путей (или URL)?
учитывая два абсолютных пути, например
/var/data/stuff/xyz.dat
/var/data
Как можно создать относительный путь, который использует второй путь в качестве своей базы? В приведенном выше примере результат должен быть: ./stuff/xyz.dat
22 ответа:
это немного окольный путь, но почему бы не использовать URI? Он имеет метод relativize, который выполняет все необходимые проверки для вас.
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 Вы можете использовать релятивизировать способ:
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 года) это было единственное решение, которое прошло мои тестовые случаи. Я не могу гарантировать, что это решение не содержит ошибок, но оно проходит включенные тестовые случаи. Метод и тесты, которые я написал, зависят от
FilenameUtils
класс Apache commons IO.решение было протестировано с 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.чистая.Ури.релятивизировать вы должны быть осведомлены о Ява ошибка: JDK-6226081 (URI должен иметь возможность релятивизировать пути с частичными корнями)
на данный момент
relativize()
методURI
будет только релятивизировать URI, когда один является префиксом другого.что по сути означает
java.net.URI.relativize
не будет создавать ".."'S для вас.
ошибка, упомянутая в ответе @Peter Mueller, адресована URIUtils на Apache HttpComponents
public static URI resolve(URI baseURI, String reference)
разрешает ссылку URI для a базовый URI. Как обойти баг в Ява.чистая.Ури ()
на Java 8 вы можете просто сделать (и в отличие от
URI
, это бесплатная ошибка):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
[EDIT] на самом деле он выводит больше ..\ потому что исходный файл не является каталогом. Правильное решение для моего случая:
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 получает количество каталогов для обратного отслеживания неправильно - это должна быть длина базового пути минус количество общих элементов пути, минус один (для последнего элемента пути, который является либо именем файла, либо конечным
""
создается с помощью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
-- thec
находится в том же слоте в обоих массивах, но не является фактическим совпадением.все эти решения не имеют возможности обрабатывать пути, которые не могут быть релятивизированы друг к другу, потому что они имеют несовместимые корни, такие как
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); }
На самом деле мой другой ответ не работал, если целевой путь не был дочерним для базового пути.
Это должно работать.
public class RelativePathFinder { public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { // find common path String[] target = targetPath.split(pathSeparator); String[] base = basePath.split(pathSeparator); 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++; } } String relative = ""; // is the target a child directory of the base directory? // i.e., target = /a/b/c/d, base = /a/b/ if (commonIndex == base.length) { relative = "." + pathSeparator + targetPath.substring(common.length()); } else { // determine how many directories we have to backtrack for (int i = 1; i <= commonIndex; i++) { relative += ".." + pathSeparator; } relative += targetPath.substring(common.length()); } return relative; } public static String getRelativePath(String targetPath, String basePath) { return getRelativePath(targetPath, basePath, File.pathSeparator); } }
public class RelativePathFinderTest extends TestCase { public void testGetRelativePath() { assertEquals("./stuff/xyz.dat", RelativePathFinder.getRelativePath( "/var/data/stuff/xyz.dat", "/var/data/", "/")); assertEquals("../../b/c", RelativePathFinder.getRelativePath("/a/b/c", "/a/x/y/", "/")); } }
круто!! Мне нужно немного кода, как это, но для сравнения путей к каталогам на машинах Linux. Я обнаружил, что это не работает в ситуациях, когда родительский каталог был целью.
вот дружественная к каталогу версия метода:
public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { boolean isDir = false; { File f = new File(targetPath); isDir = f.isDirectory(); } // 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 - (isDir?0:1); /* only subtract 1 if it is a file. */ // 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; } } //if we are comparing directories then we if (targetPath.length() > common.length()) { //it's OK, it isn't a directory relative += targetPath.substring(common.length()); } return relative; }
Я предполагаю, что у вас есть fromPath (абсолютный путь к папке), и toPath (абсолютный путь для папки / файла), и вы ищете путь, который представляет файл/папку в toPath как относительный путь от fromPath (ваш текущий рабочий каталог fromPath) тогда что-то вроде этого должно работать:
public static String getRelativePath(String fromPath, String toPath) { // This weirdness is because a separator of '/' messes with String.split() String regexCharacter = File.separator; if (File.separatorChar == '\') { regexCharacter = "\\"; } String[] fromSplit = fromPath.split(regexCharacter); String[] toSplit = toPath.split(regexCharacter); // Find the common path int common = 0; while (fromSplit[common].equals(toSplit[common])) { common++; } StringBuffer result = new StringBuffer("."); // Work your way up the FROM path to common ground for (int i = common; i < fromSplit.length; i++) { result.append(File.separatorChar).append(".."); } // Work your way down the TO path for (int i = common; i < toSplit.length; i++) { result.append(File.separatorChar).append(toSplit[i]); } return result.toString(); }
много ответов уже здесь, но я обнаружил, что они не обрабатывают все случаи, например, база и цель одинаковы. Эта функция принимает базу каталог и целевой путь и возвращает относительный путь. Если относительный путь не существует, возвращается целевой путь. Файл.разделитель не нужен.
public static String getRelativePath (String baseDir, String targetPath) { String[] base = baseDir.replace('\', '/').split("\/"); targetPath = targetPath.replace('\', '/'); String[] target = targetPath.split("\/"); // Count common elements and their length. int commonCount = 0, commonLength = 0, maxCount = Math.min(target.length, base.length); while (commonCount < maxCount) { String targetElement = target[commonCount]; if (!targetElement.equals(base[commonCount])) break; commonCount++; commonLength += targetElement.length() + 1; // Directory name length plus slash. } if (commonCount == 0) return targetPath; // No common path element. int targetLength = targetPath.length(); int dirsUp = base.length - commonCount; StringBuffer relative = new StringBuffer(dirsUp * 3 + targetLength - commonLength + 1); for (int i = 0; i < dirsUp; i++) relative.append("../"); if (commonLength < targetLength) relative.append(targetPath.substring(commonLength)); return relative.toString(); }
здесь метод, который разрешает относительный путь из базового пути, независимо от того, находятся ли они в том же или в другом корне:
public static String GetRelativePath(String path, String base){ final String SEP = "/"; // if base is not a directory -> return empty if (!base.endsWith(SEP)){ return ""; } // check if path is a file -> remove last "/" at the end of the method boolean isfile = !path.endsWith(SEP); // get URIs and split them by using the separator String a = ""; String b = ""; try { a = new File(base).getCanonicalFile().toURI().getPath(); b = new File(path).getCanonicalFile().toURI().getPath(); } catch (IOException e) { e.printStackTrace(); } String[] basePaths = a.split(SEP); String[] otherPaths = b.split(SEP); // check common part int n = 0; for(; n < basePaths.length && n < otherPaths.length; n ++) { if( basePaths[n].equals(otherPaths[n]) == false ) break; } // compose the new path StringBuffer tmp = new StringBuffer(""); for(int m = n; m < basePaths.length; m ++) tmp.append(".."+SEP); for(int m = n; m < otherPaths.length; m ++) { tmp.append(otherPaths[m]); tmp.append(SEP); } // get path string String result = tmp.toString(); // remove last "/" if path is a file if (isfile && result.endsWith(SEP)){ result = result.substring(0,result.length()-1); } return result; }
private String relative(String left, String right){ String[] lefts = left.split("/"); String[] rights = right.split("/"); int min = Math.min(lefts.length, rights.length); int commonIdx = -1; for(int i = 0; i < min; i++){ if(commonIdx < 0 && !lefts[i].equals(rights[i])){ commonIdx = i - 1; break; } } if(commonIdx < 0){ return null; } StringBuilder sb = new StringBuilder(Math.max(left.length(), right.length())); sb.append(left).append("/"); for(int i = commonIdx + 1; i < lefts.length;i++){ sb.append("../"); } for(int i = commonIdx + 1; i < rights.length;i++){ sb.append(rights[i]).append("/"); } return sb.deleteCharAt(sb.length() -1).toString(); }
проходит тесты Dónal, единственное изменение-если нет общего корня, он возвращает целевой путь (он может быть уже относительным)
import static java.util.Arrays.asList; import static java.util.Collections.nCopies; import static org.apache.commons.io.FilenameUtils.normalizeNoEndSeparator; import static org.apache.commons.io.FilenameUtils.separatorsToUnix; import static org.apache.commons.lang3.StringUtils.getCommonPrefix; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.join; import java.io.File; import java.util.ArrayList; import java.util.List; public class ResourceUtils { public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { File baseFile = new File(basePath); if (baseFile.isFile() || !baseFile.exists() && !basePath.endsWith("/") && !basePath.endsWith("\")) basePath = baseFile.getParent(); String target = separatorsToUnix(normalizeNoEndSeparator(targetPath)); String base = separatorsToUnix(normalizeNoEndSeparator(basePath)); String commonPrefix = getCommonPrefix(target, base); if (isBlank(commonPrefix)) return targetPath.replaceAll("/", pathSeparator); target = target.replaceFirst(commonPrefix, ""); base = base.replaceFirst(commonPrefix, ""); List<String> result = new ArrayList<>(); if (isNotEmpty(base)) result.addAll(nCopies(base.split("/").length, "..")); result.addAll(asList(target.replaceFirst("^/", "").split("/"))); return join(result, pathSeparator); } }
Если вы пишете плагин Maven, вы можете использовать сплетение'
PathTool
:import org.codehaus.plexus.util.PathTool; String relativeFilePath = PathTool.getRelativeFilePath(file1, file2);
Если пути недоступны для JRE 1.5 runtime или maven plugin
package org.afc.util; import java.io.File; import java.util.LinkedList; import java.util.List; public class FileUtil { public static String getRelativePath(String basePath, String filePath) { return getRelativePath(new File(basePath), new File(filePath)); } public static String getRelativePath(File base, File file) { List<String> bases = new LinkedList<String>(); bases.add(0, base.getName()); for (File parent = base.getParentFile(); parent != null; parent = parent.getParentFile()) { bases.add(0, parent.getName()); } List<String> files = new LinkedList<String>(); files.add(0, file.getName()); for (File parent = file.getParentFile(); parent != null; parent = parent.getParentFile()) { files.add(0, parent.getName()); } int overlapIndex = 0; while (overlapIndex < bases.size() && overlapIndex < files.size() && bases.get(overlapIndex).equals(files.get(overlapIndex))) { overlapIndex++; } StringBuilder relativePath = new StringBuilder(); for (int i = overlapIndex; i < bases.size(); i++) { relativePath.append("..").append(File.separatorChar); } for (int i = overlapIndex; i < files.size(); i++) { relativePath.append(files.get(i)).append(File.separatorChar); } relativePath.deleteCharAt(relativePath.length() - 1); return relativePath.toString(); } }
орг.апаш.ant имеет класс FileUtils с методом getRelativePath. Сам еще не пробовал, но может быть стоит проверить это.
http://javadoc.haefelinger.it/org.apache.ant/1.7.1/org/apache/tools/ant/util/FileUtils.html#getRelativePath(java.io.File, java. io. File)