Bagaimana membangun jalur relatif di Jawa dari dua jalur absolut (atau URL)?

275

Diberikan dua jalur absolut, misalnya

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

Bagaimana cara membuat jalur relatif yang menggunakan jalur kedua sebagai dasarnya? Pada contoh di atas, hasilnya harus:./stuff/xyz.dat

VoidPointer
sumber
3
Untuk Java 7 dan yang lebih baru, lihat jawaban @ VitaliiFedorenko.
Andy Thomas
1
tl; dr jawab: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (yang, omong-omong, tampaknya berfungsi dengan baik dengan misalnya "../" untuk saya di Jawa 8 , jadi ...)
Andrew

Jawaban:

298

Ini sedikit bundaran, tetapi mengapa tidak menggunakan URI? Ini memiliki metode relativize yang melakukan semua pemeriksaan yang diperlukan untuk Anda.

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"

Harap dicatat bahwa untuk jalur file sudah ada java.nio.file.Path#relativizesejak Java 1.7, seperti yang ditunjukkan oleh @Jirka Meluzin di jawaban lain .

Adam Crume
sumber
17
Lihat jawaban Peter Mueller. relativize () tampak sangat rusak untuk semua kecuali kasus yang paling sederhana.
Dave Ray
11
Yap, itu hanya berfungsi jika jalur dasar adalah induk dari jalur pertama. Jika Anda memerlukan beberapa hierarki mundur seperti "../../relativepath", itu tidak akan berfungsi. Saya menemukan solusi: mrpmorris.blogspot.com/2007/05/...
Aurelien Ribon
4
Seperti @VitaliiFedorenko menulis: gunakan java.nio.file.Path#relativize(Path), itu hanya berfungsi dengan titik ganda induk dan semuanya.
Campa
Pertimbangkan untuk menggunakan toPath()bukan toURI(). Ini sangat bisa membuat barang-barang seperti "..\..". Tetapi waspadai java.lang.IllegalArgumentException: 'other' has different rootpengecualian ketika meminta jalur relatif dari "C:\temp"ke "D:\temp".
Igor
Ini tidak berfungsi seperti yang diharapkan, ini mengembalikan data / stuff / xyz.dat dalam test case saya.
izin
238

Karena Java 7 Anda dapat menggunakan metode 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);
    }

}

Keluaran:

stuff/xyz.dat
Vitalii Fedorenko
sumber
3
Bagus, pendek, tanpa lib ekstra +1. Solusi Adam Crume (hit 1) tidak lulus tes saya dan jawaban berikutnya (hit2) "Satu-satunya 'Bekerja' Solusi" menambahkan jar baru DAN lebih banyak kode daripada implementasi saya, saya menemukan ini di sini setelah itu ... lebih baik daripada tidak pernah .- )
hokr
1
Tapi hati-hati dengan masalah ini .
ben3000
1
Diperiksa bahwa ini menangani penambahan ..jika perlu (benar).
Owen
Sayangnya, Android tidak termasuk java.nio.file:(
Nathan Osman
1
Saya menemukan Anda mendapatkan hasil yang aneh jika "pathBase" tidak "dinormalisasi" sebelum "relativize". Meskipun baik dalam contoh ini, saya akan melakukan pathBase.normalize().relativize(pathAbsolute);sebagai aturan umum.
pstanton
77

Pada saat penulisan (Juni 2010), ini adalah satu-satunya solusi yang lulus ujian saya. Saya tidak dapat menjamin bahwa solusi ini bebas bug, tetapi tidak lulus uji kasus yang disertakan. Metode dan tes yang saya tulis tergantung pada FilenameUtilskelas dari Apache commons IO .

Solusinya diuji dengan Java 1.4. Jika Anda menggunakan Java 1.5 (atau lebih tinggi) Anda harus mempertimbangkan untuk mengganti StringBufferdengan StringBuilder(jika Anda masih menggunakan Java 1.4, Anda harus mempertimbangkan penggantian majikan).

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

Kasus uji yang lolos adalah

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
sumber
5
Bagus! Satu hal, meskipun, itu rusak jika basis dan target adalah sama - string umum dibuat untuk berakhir di pemisah, yang tidak dimiliki jalur target yang dinormalisasi, sehingga panggilan substring meminta satu digit terlalu banyak. Pikirkan saya memperbaikinya dengan menambahkan berikut sebelum dua baris terakhir dari fungsi: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Erhannis
4
Mengatakan ini adalah satu-satunya solusi yang berfungsi adalah menyesatkan. Jawaban lain berfungsi lebih baik (jawaban ini macet ketika basis dan targetnya sama), lebih sederhana dan tidak bergantung pada commons-io.
NateS
26

Saat menggunakan java.net.URI.relativize Anda harus mewaspadai bug Java: JDK-6226081 (URI harus dapat merelatifkan path dengan root parsial)

Saat ini, relativize()metode URIhanya akan merelatifkan URI ketika satu merupakan awalan dari yang lain.

Yang pada dasarnya berarti java.net.URI.relativizetidak akan membuat ".." untuk Anda.

Christian K.
sumber
6
Menjijikan. Ada solusi untuk ini, rupanya: stackoverflow.com/questions/204784
skaffman
Paths.get (startPath) .relativize (Paths.get (endPath)). ToString‌ () tampaknya berfungsi dengan baik dengan misalnya "../" untuk saya di Jawa 8.
Andrew
@ skaffman Anda yakin? Jawaban ini merujuk bug JDK-6226081, tetapi URIUtils.resolve()menyebutkan JDK-4708535. Dan dari kode sumber, saya tidak melihat apa pun yang terkait dengan penelusuran ulang (yaitu ..segmen). Apakah Anda membingungkan kedua bug?
Garret Wilson
JDK-6920138 ditandai sebagai duplikat JDK-4708535.
Christian K.
18

Di Java 7 dan yang lebih baru, Anda cukup menggunakan (dan berbeda dengan URIitu, bebas bug):

Path#relativize(Path)
ruller
sumber
17

Bug yang dimaksud dalam jawaban lain diatasi oleh URIUtils di Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Menyelesaikan referensi URI terhadap URI basis. Mengatasi bug di java.net.URI ()

skaffman
sumber
Tidakkah metode penyelesaian menghasilkan URI absolut dari basis dan jalur relatif? Bagaimana metode ini membantu?
Mengejar
10

Jika Anda tahu string kedua adalah bagian dari yang pertama:

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

atau jika Anda benar-benar menginginkan periode di awal seperti pada contoh Anda:

String s3 = ".".concat(s1.substring(s2.length()));
Keeg
sumber
3
String s3 = "." + s1.substring (s2.length ()); IMO sedikit lebih mudah dibaca
Dónal
10

Rekursi menghasilkan solusi yang lebih kecil. Ini melempar pengecualian jika hasilnya tidak mungkin (misalnya disk Windows berbeda) atau tidak praktis (root hanya direktori umum.)

/**
 * 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));
    }
}
Bakar L.
sumber
getCanonicalPath bisa jadi sangat berat, jadi solusi ini tidak bisa direkomendasikan ketika Anda perlu memproses ratusan ribu catatan. Misalnya saya memiliki beberapa file daftar yang memiliki hingga jutaan catatan dan sekarang saya ingin memindahkannya untuk menggunakan jalur relatif untuk portabilitas.
user2305886
8

Berikut ini solusi gratis perpustakaan lainnya:

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

Keluaran

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

[EDIT] sebenarnya ini menghasilkan lebih banyak .. \ karena sumbernya adalah file bukan direktori. Solusi yang tepat untuk kasus saya adalah:

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
sumber
6

Versi saya secara longgar didasarkan pada versi Matt dan Steve :

/**
 * 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
sumber
2
+1 berfungsi untuk saya. Hanya koreksi kecil: alih-alih "/".length()Anda harus menggunakan separator.length
leonbloy
5

Solusi Matt B membuat jumlah direktori untuk mundur dengan salah - itu harus menjadi panjang jalur dasar dikurangi jumlah elemen jalur umum, minus satu (untuk elemen jalur terakhir, yang dapat berupa nama file atau trailing yang ""dihasilkan oleh split) . Itu terjadi untuk bekerja dengan /a/b/c/dan /a/x/y/, tetapi mengganti argumen dengan /m/n/o/a/b/c/dan/m/n/o/a/x/y/ dan Anda akan melihat masalahnya.

Juga, ini membutuhkan bagian else breakdalam pertama untuk loop, atau itu akan salah jalan yang kebetulan memiliki nama direktori yang cocok, seperti /a/b/c/d/dan /x/y/c/z- yang cada di slot yang sama di kedua array, tetapi bukan pertandingan yang sebenarnya.

Semua solusi ini tidak memiliki kemampuan untuk menangani jalur yang tidak dapat direlatifikasi satu sama lain karena mereka memiliki akar yang tidak kompatibel, seperti C:\foo\bardan D:\baz\quux. Mungkin hanya masalah pada Windows, tetapi patut dicatat.

Saya menghabiskan jauh lebih lama untuk ini daripada yang saya maksudkan, tapi tidak apa-apa. Saya benar-benar membutuhkan ini untuk bekerja, jadi terima kasih kepada semua orang yang telah ikut serta, dan saya yakin akan ada koreksi pada versi ini juga!

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

Dan berikut ini adalah tes untuk mencakup beberapa kasus:

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
sumber
4

Sebenarnya jawaban saya yang lain tidak berfungsi jika jalur target bukan anak dari jalur dasar.

Ini seharusnya bekerja.

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

}
matt b
sumber
2
Alih-alih File.pathSeparator harus File.separator. pathSeparator harus digunakan hanya untuk split (regex), seperti untuk regex "////" (win path regex), jalur hasil akan salah.
Alex Ivasyuv
3

Keren!! Saya perlu sedikit kode seperti ini tetapi untuk membandingkan jalur direktori pada mesin Linux. Saya menemukan bahwa ini tidak berfungsi dalam situasi di mana direktori induk adalah targetnya.

Berikut adalah versi metode yang ramah direktori:

 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
sumber
2

Saya berasumsi Anda memiliki fromPath (jalur absolut untuk folder), dan toPath (jalur absolut untuk folder / file), dan Anda sedang mencari jalur yang dengan mewakili file / folder di toPath sebagai jalur relatif from fromath (direktori kerja Anda saat ini adalah fromPath ) maka sesuatu seperti ini seharusnya berfungsi:

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
sumber
1

Sudah banyak jawaban di sini, tetapi saya menemukan mereka tidak menangani semua kasus, seperti basis dan target sama. Fungsi ini mengambil direktori basis dan jalur target dan mengembalikan jalur relatif. Jika tidak ada jalur relatif, jalur target dikembalikan. File.separator tidak perlu.

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
sumber
0

Di sini metode yang menyelesaikan jalur relatif dari jalur dasar tanpa memperhatikan mereka berada di root yang sama atau berbeda:

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
sumber
0

Lulus tes Dónal, satu-satunya perubahan - jika tidak ada root yang sama, ia mengembalikan jalur target (mungkin sudah relatif)

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);
    }
}
Mike
sumber
0

Jika Anda menulis plugin Maven, Anda dapat menggunakan Plexus 'PathTool :

import org.codehaus.plexus.util.PathTool;

String relativeFilePath = PathTool.getRelativeFilePath(file1, file2);
Ben Hutchison
sumber
0

Jika Paths tidak tersedia untuk JRE 1.5 runtime atau plugin pakar

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
sumber
-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
sumber
-2

Kode psuedo:

  1. Pisahkan string dengan pemisah jalur ("/")
  2. Temukan jalur umum terbesar dengan mengulangi melalui hasil string split (sehingga Anda akan berakhir dengan "/ var / data" atau "/ a" dalam dua contoh Anda)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
matt b
sumber
2
Jawaban ini adalah hack terbaik. Bagaimana dengan windows?
Qix - MONICA DISEBUTKAN