Jak zbudować ścieżkę względną w Javie z dwóch ścieżek bezwzględnych (lub adresów URL)?

275

Biorąc pod uwagę dwie ścieżki bezwzględne, np

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

Jak można utworzyć ścieżkę względną, która będzie używać drugiej ścieżki jako podstawy? W powyższym przykładzie wynik powinien wynosić:./stuff/xyz.dat

VoidPointer
źródło
3
W przypadku wersji Java 7 i nowszych zobacz odpowiedź @ VitaliiFedorenko.
Andy Thomas,
1
tl; dr answer: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (który, nawiasem mówiąc, wydaje się działać dobrze z np. „../” dla mnie w Javie 8 , więc ...)
Andrew

Odpowiedzi:

297

To trochę rondo, ale dlaczego nie użyć URI? Ma metodę relatywizacji, która wykonuje wszystkie niezbędne kontrole.

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"

Pamiętaj, że ścieżkę do pliku można znaleźć w java.nio.file.Path#relativizejęzyku Java od wersji 1.7, na co zwrócił uwagę Jirka Meluzin w drugiej odpowiedzi .

Adam Crume
źródło
17
Zobacz odpowiedź Petera Muellera. relativize () wydaje się dość zepsuty we wszystkich przypadkach oprócz najprostszych.
Dave Ray
11
Tak, działa tylko wtedy, gdy ścieżka podstawowa jest rodzicem pierwszej ścieżki. Jeśli potrzebujesz hierarchicznej wstecz, takiej jak „../../relativepath”, to nie zadziała. Znalazłem rozwiązanie: mrpmorris.blogspot.com/2007/05/…
Aurelien Ribon
4
Jak napisał @VitaliiFedorenko: użyj java.nio.file.Path#relativize(Path), działa tylko z nadrzędnymi podwójnymi kropkami i wszystkim.
Campa,
Rozważ użycie toPath()zamiast toURI(). Doskonale potrafi tworzyć takie rzeczy jak "..\..". Ale pamiętaj o java.lang.IllegalArgumentException: 'other' has different rootwyjątku, prosząc o względną ścieżkę od "C:\temp"do "D:\temp".
Igor
To nie działa zgodnie z oczekiwaniami, zwraca dane / stuff / xyz.dat w moim przypadku testowym.
nieprzyzwoity
238

Od wersji Java 7 można używać metody relatywizacji :

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

}

Wynik:

stuff/xyz.dat
Vitalii Fedorenko
źródło
3
Ładne, krótkie, bez dodatkowych lib +1. Rozwiązanie Adama Crume'a (hit 1) nie przechodzi moich testów, a następna odpowiedź (hit2) „Jedyne„ działające ”rozwiązanie dodaje nowy słoik ORAZ zawiera więcej kodu niż moja implementacja, znajduję to tutaj później ... lepiej niż nigdy. - )
hokr
1
Ale uważaj na ten problem .
ben3000 12.04.16
1
Sprawdzono, czy obsługuje dodawanie w ..razie potrzeby (tak jest).
Owen,
Niestety, Android nie obejmuje java.nio.file:(
Nathan Osman
1
Odkryłem, że masz dziwne wyniki, jeśli „pathBase” nie jest „znormalizowany” przed „relatywizacją”. Chociaż w tym przykładzie jest w porządku, postąpiłbym pathBase.normalize().relativize(pathAbsolute);zgodnie z ogólną zasadą.
pstanton
77

W momencie pisania tego tekstu (czerwiec 2010 r.) Było to jedyne rozwiązanie, które przeszło moje testy. Nie mogę zagwarantować, że to rozwiązanie jest wolne od błędów, ale przechodzi pomyślnie dołączone przypadki testowe. Metoda i testy, które napisałem, zależą od FilenameUtilsklasy z Apache commons IO .

Rozwiązanie przetestowano w Javie 1.4. Jeśli używasz języka Java 1.5 (lub nowszego), powinieneś rozważyć zastąpienie StringBuffergo StringBuilder(jeśli nadal używasz języka Java 1.4, powinieneś rozważyć zmianę pracodawcy).

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

Przypadki testowe, które przechodzi, są następujące

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
źródło
5
Miły! Jedna rzecz się jednak psuje, jeśli podstawa i cel są takie same - wspólny ciąg znaków kończy się na separatorze, którego nie ma znormalizowanej ścieżki docelowej, więc wywołanie podła pyta o jedną za dużo cyfr. Myślę, że naprawiłem to, dodając następujące przed dwoma ostatnimi wierszami funkcji: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Erhannis,
4
Mówienie, że to jedyne działające rozwiązanie, wprowadza w błąd. Inne odpowiedzi działają lepiej (ta odpowiedź ulega awarii, gdy baza i cel są takie same), są prostsze i nie polegają na commons-io.
NateS
26

Korzystając z java.net.URI.relativize, powinieneś zdawać sobie sprawę z błędu Java: JDK-6226081 (URI powinien mieć możliwość relatywizacji ścieżek z częściowymi korzeniami)

W tej chwili relativize()metoda URIrelatywizuje identyfikatory URI tylko wtedy, gdy jeden jest prefiksem drugiego.

Co w istocie oznacza, java.net.URI.relativizeże nie stworzy dla ciebie „..”.

Christian K.
źródło
6
Paskudny. Prawdopodobnie istnieje obejście tego problemu: stackoverflow.com/questions/204784/…
skaffman
Paths.get (startPath) .relativize (Paths.get (endPath)). Wydaje się, że toString‌ () działa dobrze z np. „../” dla mnie w Javie 8.
Andrew
@skaffman jesteś pewien? Ta odpowiedź odwołuje się do błędu JDK-6226081, ale URIUtils.resolve()wspomina o JDK-4708535. A z kodu źródłowego nie widzę nic związanego z cofaniem (tj. ..Segmentami). Czy pomyliłeś te dwa błędy?
Garret Wilson
JDK-6920138 jest oznaczony jako duplikat JDK-4708535.
Christian K.
17

Błąd, o którym mowa w innej odpowiedzi, został rozwiązany przez URIUtils w Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Rozwiązuje odwołanie URI względem podstawowego URI. Obejście problemu w java.net.URI ()

skaffman
źródło
Czy metoda resolver nie generuje bezwzględnego identyfikatora URI na podstawie ścieżki podstawowej i względnej? Jak ta metoda mogłaby pomóc?
Chase
17

W Javie 7 i nowszych możesz po prostu użyć (i w przeciwieństwie do URItego, jest wolne od błędów):

Path#relativize(Path)
rmuller
źródło
10

Jeśli wiesz, że drugi ciąg jest częścią pierwszego:

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

lub jeśli naprawdę chcesz kropkę na początku, jak w twoim przykładzie:

String s3 = ".".concat(s1.substring(s2.length()));
Keeg
źródło
3
Ciąg s3 = "." + s1. podciąg (s2. długość ()); jest nieco bardziej czytelny IMO
Dónal
10

Rekurencja daje mniejsze rozwiązanie. Powoduje to wyjątek, jeśli wynik jest niemożliwy (np. Inny dysk Windows) lub niepraktyczny (root jest tylko wspólnym katalogiem).

/**
 * 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.
źródło
getCanonicalPath może mieć dużą wagę, więc nie można polecać tego rozwiązania, gdy trzeba przetworzyć sto tysięcy rekordów. Na przykład mam kilka plików z listami zawierających do miliona rekordów, a teraz chcę je przenieść, aby użyć ścieżki względnej do przenoszenia.
user2305886,
8

Oto rozwiązanie inne biblioteki za darmo:

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

Wyjścia

..\..\..\..\d\e\f2.txt

[EDYCJA] tak naprawdę wyświetla więcej ... \ ze względu na to, że źródłem jest plik, a nie katalog. Prawidłowe rozwiązanie dla mojej sprawy to:

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);
Jirka Meluzin
źródło
6

Moja wersja jest luźno oparta na wersjach Matta i Steve'a :

/**
 * 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());
 }
Gili
źródło
2
+1 działa dla mnie. Tylko niewielka korekta: zamiast tego "/".length()należy użyć separatora.
Długość
5

Rozwiązanie Matta B powoduje nieprawidłową liczbę katalogów do śledzenia wstecznego - powinna to być długość ścieżki bazowej minus liczba wspólnych elementów ścieżki, minus jeden (dla ostatniego elementu ścieżki, który jest nazwą pliku lub znakiem końcowym ""generowanym przez split) . Zdarza się, że działa z /a/b/c/i /a/x/y/, ale zamień argumenty na /m/n/o/a/b/c/i /m/n/o/a/x/y/zobaczysz problem.

Potrzebuje również else breakwewnątrz pierwszej pętli for, w przeciwnym razie będzie źle obsługiwać ścieżki, które akurat mają pasujące nazwy katalogów, takie jak /a/b/c/d/i /x/y/c/z- cjest w tym samym gnieździe w obu tablicach, ale nie jest faktycznym dopasowaniem.

Wszystkie te rozwiązania nie są w stanie obsłużyć ścieżek, których nie można relatywizować, ponieważ mają niekompatybilne korzenie, takie jak C:\foo\bari D:\baz\quux. Prawdopodobnie tylko problem w systemie Windows, ale warto zauważyć.

Spędziłem na tym znacznie dłużej, niż zamierzałem, ale to w porządku. Naprawdę potrzebowałem tego do pracy, więc dziękuję wszystkim, którzy się do niego przyłączyli, i jestem pewien, że będą także poprawki w tej wersji!

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

A oto testy obejmujące kilka przypadków:

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
źródło
4

Właściwie moja inna odpowiedź nie zadziałała, jeśli ścieżka docelowa nie była dzieckiem ścieżki podstawowej.

To powinno działać.

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

}
matowy b
źródło
2
Zamiast File.pathSeparator powinien być File.separator. pathSeparator powinien używać tylko do dzielenia (wyrażenia regularnego), ponieważ w przypadku wyrażenia regularnego „////” (wyrażanie regularne ścieżki wygranej) ścieżka wyniku będzie niepoprawna.
Alex Ivasyuv
3

Chłodny!! Potrzebuję trochę takiego kodu, ale do porównania ścieżek katalogów na komputerach z systemem Linux. Odkryłem, że to nie działało w sytuacjach, w których katalog nadrzędny był celem.

Oto wersja metody przyjazna dla katalogu:

 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;
}
Rachel
źródło
2

Zakładam, że masz fromPath (bezwzględną ścieżkę do folderu) i toPath (bezwzględną ścieżkę do folderu / pliku) i szukasz ścieżki, która reprezentuje plik / folder w toPath jako ścieżkę względną from fromPath (bieżący katalog roboczy to fromPath ), wtedy coś takiego powinno działać:

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();
}
Steve Armstrong
źródło
1

Wiele odpowiedzi już tu jest, ale okazało się, że nie poradziły sobie ze wszystkimi przypadkami, takimi jak baza i cel są takie same. Ta funkcja pobiera katalog podstawowy i ścieżkę docelową i zwraca ścieżkę względną. Jeśli nie istnieje ścieżka względna, zwracana jest ścieżka docelowa. File.separator jest niepotrzebny.

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();
}
NateS
źródło
0

Tutaj metoda, która rozwiązuje ścieżkę względną ze ścieżki podstawowej, niezależnie od tego, czy znajdują się w tym samym lub innym katalogu głównym:

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
źródło
0

Przechodzi testy Dónala, jedyna zmiana - jeśli nie ma wspólnego katalogu głównego, zwraca ścieżkę docelową (może być już względna)

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);
    }
}
Mikrofon
źródło
0

Jeśli piszesz wtyczkę Maven, możesz użyć PlexusaPathTool :

import org.codehaus.plexus.util.PathTool;

String relativeFilePath = PathTool.getRelativeFilePath(file1, file2);
Ben Hutchison
źródło
0

Jeśli ścieżki nie są dostępne dla środowiska wykonawczego JRE 1.5 lub wtyczki 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
źródło
-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
źródło
-2

Kod Psuedo:

  1. Podziel ciągi znaków według separatora ścieżki („/”)
  2. Znajdź największą wspólną ścieżkę, iterując wynik podzielonego łańcucha (aby w dwóch przykładach uzyskać wynik „/ var / data” lub „/ a”)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
matowy b
źródło
2
Ta odpowiedź to w najlepszym razie hack. Co z oknami?
Qix - MONICA MISTREATED