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

275

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

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

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

VoidPointer
источник
3
Для Java 7 и более поздних версий смотрите ответ @ VitaliiFedorenko.
Энди Томас
1
tl; dr ответ: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (который, кстати, вроде бы работает нормально, например, "../" для меня в Java 8 итак ...)
Андрей

Ответы:

298

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

Адам Крум
источник
17
Смотрите ответ Питера Мюллера. релятивизация () выглядит довольно неработоспособной для всех, кроме самых простых случаев.
Дейв Рэй
11
Да, это работает, только если базовый путь является родителем первого пути. Если вам нужна иерархическая обратная связь, например "../../relativepath", это не сработает. Я нашел решение: mrpmorris.blogspot.com/2007/05/…
Рибон,
4
Как писал @VitaliiFedorenko: используйте java.nio.file.Path#relativize(Path), он просто работает с родительскими двойными точками и все.
Кампа
Попробуйте использовать toPath()вместо toURI(). Он прекрасно умеет создавать такие вещи, как "..\..". Но помните об java.lang.IllegalArgumentException: 'other' has different rootисключении при запросе относительного пути от "C:\temp"к "D:\temp".
Игорь
Это не работает должным образом, в моем тестовом случае он возвращает data / stuff / xyz.dat.
unbekant
238

Начиная с 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
Виталий Федоренко
источник
3
Хороший, короткий, без лишних lib +1. Решение Адама Крума (хит 1) не проходит мои тесты, и следующий ответ (хит2) «Единственное« рабочее »решение» добавляет новый jar-файл и больше кода, чем моя реализация, я нахожу это здесь потом ... лучше, чем никогда .- )
Хокр
1
Но остерегайтесь этой проблемы .
ben3000
1
Проверено, что это обрабатывает добавление, ..где необходимо (это делает).
Оуэн
К сожалению, Android не включает java.nio.file:(
Натан Осман
1
Я обнаружил, что вы получите странные результаты, если «pathBase» не «нормализован» до «релятивизировать». Хотя хорошо в этом примере, я бы сделал pathBase.normalize().relativize(pathAbsolute);как общее правило.
pstanton
77

На момент написания статьи (июнь 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
    }
}
Dónal
источник
5
Ницца! Одна вещь, однако, ломается, если основание и цель одинаковы - строка общего должна заканчиваться разделителем, которого нет в нормализованном целевом пути, поэтому вызов подстроки запрашивает слишком много цифр. Думаю, я исправил это, добавив следующее перед двумя последними строками функции: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Эрханнис
4
Сказать, что это единственное рабочее решение, вводит в заблуждение. Другие ответы работают лучше (этот ответ дает сбой, когда база и цель совпадают), проще и не зависят от commons-io.
NateS
26

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

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

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

Кристиан К.
источник
6
Насти. По-видимому, для этого есть обходной путь: stackoverflow.com/questions/204784/…
skaffman
Paths.get (startPath) .relativize (Paths.get (endPath)). ToString‌ (), кажется, работает очень хорошо, например, "../" для меня в Java 8.
Эндрю
@skaffman ты уверен? Этот ответ ссылается на ошибку JDK-6226081, но URIUtils.resolve()упоминает JDK-4708535. И из исходного кода я не вижу ничего, связанного с возвратом (то есть ..сегменты). Вы перепутали две ошибки?
Гаррет Уилсон
JDK-6920138 помечен как дубликат JDK-4708535.
Кристиан К.
18

В Java 7 и более поздних версиях вы можете просто использовать (и, в отличие от этого URI, он не содержит ошибок):

Path#relativize(Path)
rmuller
источник
17

Ошибка, упомянутая в другом ответе , устранена URIUtils в Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Разрешает ссылку URI на базовый URI. Обходной путь для ошибки в java.net.URI ()

skaffman
источник
Разве метод разрешения не генерирует абсолютный URI из базы и относительного пути? Как этот метод поможет?
Погоня
10

Если вы знаете, что вторая строка является частью первой:

String s1 = "/var/data/stuff/xyz.dat";
String s2 = "/var/data";
String s3 = s1.substring(s2.length());

или если вы действительно хотите точку в начале, как в вашем примере:

String s3 = ".".concat(s1.substring(s2.length()));
Keeg
источник
3
Строка s3 = "." + s1.substring (s2.length ()); немного более читабелен ИМО
Донал
10

Рекурсия дает меньшее решение. Это вызывает исключение, если результат невозможен (например, другой диск 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));
    }
}
Burn L.
источник
getCanonicalPath может иметь большой вес, поэтому это решение не рекомендуется, если вам нужно обработать сотни тысяч записей. Например, у меня есть несколько файлов списков, содержащих до миллиона записей, и теперь я хочу переместить их, чтобы использовать относительный путь для переносимости.
user2305886
8

Вот решение другой библиотеки бесплатно:

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);
Йирка Мелузин
источник
6

Моя версия свободно основана на версиях Мэтта и Стива :

/**
 * 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());
 }
Гили
источник
2
+1 работает на меня. Только незначительное исправление: вместо "/".length()вас следует использовать separator.length
leonbloy
5

Решение 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- 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);
}
Matuszek
источник
4

На самом деле мой другой ответ не сработал, если целевой путь не был дочерним по отношению к базовому пути.

Это должно работать.

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/", "/"));
    }

}
Мэтт Б
источник
2
Вместо File.pathSeparator должен быть File.separator. pathSeparator следует использовать только для разбиения (регулярное выражение), так как для регулярного выражения "////" (регулярное выражение win path) путь результата будет неверным.
Алекс Ивасёв
3

Прохладно!! Мне нужно немного такого кода, но для сравнения путей к каталогам на машинах 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;
}
Рейчел
источник
2

Я предполагаю, что у вас есть fromPath (абсолютный путь к папке) и toPath (абсолютный путь к папке / файлу), и вы ищете путь, который представляет файл / папку в toPath как относительный путь from 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();
}
Стив Армстронг
источник
1

Множество ответов уже здесь, но я обнаружил, что они не обрабатывают все случаи, такие как основание и цель одинаковы. Эта функция берет базовый каталог и целевой путь и возвращает относительный путь. Если относительный путь не существует, целевой путь возвращается. File.separator не нужен.

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();
}
ягодицы
источник
0

Здесь метод, который разрешает относительный путь из базового пути, независимо от того, находятся ли они в том же или в другом корне:

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;
}
pedromateo
источник
0

Проходит тесты Доналя, единственное изменение - если нет общего корня, он возвращает целевой путь (он может быть уже относительным)

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);
    }
}
Майк
источник
0

Если вы пишете плагин Maven, вы можете использовать Plexus 'PathTool :

import org.codehaus.plexus.util.PathTool;

String relativeFilePath = PathTool.getRelativeFilePath(file1, file2);
Бен Хатчисон
источник
0

Если Paths недоступен для среды выполнения JRE 1.5 или плагина maven

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();
    }

}
alftank
источник
-1
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();
}
terensu
источник
-2

Псевдопользователей-код:

  1. Разделить строки с помощью разделителя пути ("/")
  2. Найдите наибольший общий путь путем итерации по результату строки разбиения (так что в итоге вы получите «/ var / data» или «/ a» в ваших двух примерах)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
Мэтт Б
источник
2
Этот ответ в лучшем случае взломать. А как насчет окон?
Qix - МОНИКА БЫЛА НЕПРАВИЛЬНОЙ