Вопрос-ответ

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

Как построить относительный путь в Java из двух абсолютных путей (или URL-адресов)?

Учитывая два абсолютных пути, например

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

Как можно создать относительный путь, который использует второй путь в качестве основы? В приведенном выше примере результат должен быть: ./stuff/xyz.dat

Переведено автоматически
Ответ 1

Это небольшой обходной путь, но почему бы не использовать 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 в другом ответе.

Ответ 2

Начиная с 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
Ответ 3

На момент написания (июнь 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
}
}
Ответ 4

При использовании java.net.URI.relativize вы должны знать об ошибке Java: JDK-6226081 (URI должен иметь возможность относительной привязки путей с частичными корнями)


На данный момент relativize() метод URI будет релятивизировать URI только тогда, когда один является префиксом другого.


Что по сути означает, что java.net.URI.relativize не будет создавать ".." для вас.

2024-02-29 23:56 java file