Come costruire un percorso relativo in Java da due percorsi assoluti (o URL)?


275

Dati due percorsi assoluti, ad es

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

Come si può creare un percorso relativo che utilizza il secondo percorso come base? Nell'esempio sopra, il risultato dovrebbe essere:./stuff/xyz.dat


3
Per Java 7 e versioni successive, vedere la risposta di @ VitaliiFedorenko.
Andy Thomas,

1
tl; dr answer: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (che, a proposito, sembra funzionare bene con ad es. "../" per me in Java 8 , quindi ...)
Andrew,

Risposte:


298

È una piccola rotonda, ma perché non usare l'URI? Ha un metodo di relativizzazione che fa tutti i controlli necessari per te.

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"

Si prega di notare che per il percorso del file c'è java.nio.file.Path#relativizedal Java 1.7, come fuori punte da @Jirka Meluzin nel l'altra risposta .


17
Vedi la risposta di Peter Mueller. relativize () sembra piuttosto rotto per tutti tranne i casi più semplici.
Dave Ray,

11
Sì, funziona solo se il percorso di base è un genitore del primo percorso. Se hai bisogno di una gerarchia all'indietro come "../../relativepath", non funzionerà. Ho trovato una soluzione: mrpmorris.blogspot.com/2007/05/…
Aurelien Ribon

4
Come ha scritto @VitaliiFedorenko: usa java.nio.file.Path#relativize(Path), funziona solo con i punti doppi dei genitori e tutto il resto.
Campa,

Prendi in considerazione l'utilizzo toPath()anziché toURI(). È perfettamente in grado di creare cose come "..\..". Ma fai attenzione alle java.lang.IllegalArgumentException: 'other' has different rooteccezioni quando chiedi il percorso relativo da "C:\temp"a "D:\temp".
Igor,

Questo non funziona come previsto, restituisce data / stuff / xyz.dat nel mio caso di test.
unbekant

238

Da Java 7 è possibile utilizzare il metodo 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);
    }

}

Produzione:

stuff/xyz.dat

3
Bello, breve, senza lib extra +1. La soluzione di Adam Crume (hit 1) non supera i miei test e la prossima risposta (hit2) "L'unica soluzione funzionante" aggiunge un nuovo vaso E è più codice della mia implementazione, lo trovo qui dopo ... meglio che mai .- )
hokr,

1
Ma fai attenzione a questo problema .
ben3000,

1
Controllato che questo gestisce l'aggiunta ..dove necessario (lo fa).
Owen,

Sfortunatamente, Android non include java.nio.file:(
Nathan Osman,

1
Ho scoperto che ottieni risultati strani se "pathBase" non è "normalizzato" prima di "relativizzare". Sebbene vada bene in questo esempio, farei pathBase.normalize().relativize(pathAbsolute);come regola generale.
Pstanton,

77

Al momento della stesura (giugno 2010), questa era l'unica soluzione che ha superato i miei casi di test. Non posso garantire che questa soluzione sia priva di bug, ma supera i casi di test inclusi. Il metodo e i test che ho scritto dipendono dalla FilenameUtilsclasse di IO comune di Apache .

La soluzione è stata testata con Java 1.4. Se stai usando Java 1.5 (o versione successiva) dovresti prendere in considerazione la sostituzioneStringBuffer con StringBuilder(se stai ancora usando Java 1.4 dovresti invece considerare un cambio di datore di lavoro).

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

I casi di test che questo passa sono

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

5
Bello! Una cosa, tuttavia, si interrompe se la base e la destinazione sono uguali: la stringa comune termina in un separatore, che non ha il percorso di destinazione normalizzato, quindi la chiamata di sottostringa richiede troppe cifre. Penso di averlo corretto aggiungendo quanto segue prima delle ultime due righe della funzione: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Erhannis,

4
Dire che questa è l'unica soluzione funzionante è fuorviante. Altre risposte funzionano meglio (questa risposta si arresta in modo anomalo quando base e target sono uguali), sono più semplici e non si basano su beni comuni.
NateS,

26

Quando si utilizza java.net.URI.relativize, è necessario essere consapevoli del bug Java: JDK-6226081 (l'URI dovrebbe essere in grado di relativizzare i percorsi con radici parziali)

Al momento, il relativize()metodo di URIrelativizza gli URI solo quando uno è un prefisso dell'altro.

Il che significa essenzialmente java.net.URI.relativizeche non creerà ".." per te.


6
Cattiva. C'è una soluzione per questo, a quanto pare: stackoverflow.com/questions/204784/...
skaffman

Paths.get (startPath) .relativize (Paths.get (endPath)). ToString‌ () sembra funzionare perfettamente con ad esempio "../" per me in Java 8.
Andrew

@skaffman sei sicuro? Questa risposta fa riferimento al bug JDK-6226081, ma URIUtils.resolve()menziona JDK-4708535. E dal codice sorgente, non vedo nulla di correlato al backtracking (ad es. ..Segmenti). Hai confuso i due bug?
Garret Wilson,

JDK-6920138 è contrassegnato come duplicato di JDK-4708535.
Christian K.

18

In Java 7 e versioni successive puoi semplicemente usare (e, al contrario URI, è privo di bug):

Path#relativize(Path)

17

Il bug a cui fa riferimento un'altra risposta viene risolto da URIUtils in Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Risolve un riferimento URI rispetto a un URI di base. Soluzione per bug in java.net.URI ()


Il metodo di risoluzione non genera un URI assoluto da una base e un percorso relativo? In che modo questo metodo sarebbe d'aiuto?
Insegui il

10

Se sai che la seconda stringa fa parte della prima:

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

o se vuoi davvero il periodo all'inizio come nel tuo esempio:

String s3 = ".".concat(s1.substring(s2.length()));

3
String s3 = "." + s1.substring (s2.length ()); è leggermente più leggibile IMO
Dónal,

10

La ricorsione produce una soluzione più piccola. Ciò genera un'eccezione se il risultato è impossibile (ad es. Un diverso disco di Windows) o poco pratico (il root è solo una directory comune).

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

getCanonicalPath può essere pesante, quindi questa soluzione non può essere consigliata quando è necessario elaborare centinaia di migliaia di record. Ad esempio, ho alcuni file di elenchi con un massimo di milioni di record e ora voglio spostarli per utilizzare il percorso relativo per la portabilità.
user2305886,

8

Ecco una soluzione gratuita per altre librerie:

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

Uscite

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

[EDIT] in realtà emette su più .. \ perché il sorgente è un file non una directory. La soluzione corretta per il mio caso è:

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

La mia versione è vagamente basata sulle versioni di Matt e 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());
 }

2
+1 funziona per me. Solo una correzione minore: al posto "/".length()tuo dovresti usare separator.length
leonbloy

5

La soluzione di Matt B fa in modo che il numero di directory torni indietro - dovrebbe essere la lunghezza del percorso di base meno il numero di elementi del percorso comuni, meno uno (per l'ultimo elemento del percorso, che è un nome di file o un trailing ""generato da split) . Capita di funzionare con /a/b/c/e /a/x/y/, ma sostituisce gli argomenti con /m/n/o/a/b/c/e/m/n/o/a/x/y/ e vedrai il problema.

Inoltre, ha bisogno di un else breakdentro il primo per il ciclo, o gestirà male i percorsi che hanno i nomi di directory corrispondenti, come /a/b/c/d/e /x/y/c/z- ilc è nello stesso slot in entrambi gli array, ma non è una corrispondenza effettiva.

Tutte queste soluzioni mancano della capacità di gestire percorsi che non possono essere relativizzati l'uno con l'altro perché hanno radici incompatibili, come C:\foo\bareD:\baz\quux . Probabilmente solo un problema su Windows, ma vale la pena notare.

Ho speso molto più tempo di quello che volevo, ma va bene. In realtà ne avevo bisogno per lavoro, quindi grazie a tutti coloro che sono intervenuti, e sono sicuro che ci saranno correzioni anche per questa versione!

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

E qui ci sono test per coprire diversi casi:

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

4

In realtà la mia altra risposta non ha funzionato se il percorso target non era figlio del percorso base.

Questo dovrebbe funzionare.

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
Invece di File.pathSeparator dovrebbe essere File.separator. pathSeparator dovrebbe usare solo per split (regex), come per regex "////" (win path regex), il percorso del risultato sarà errato.
Alex Ivasyuv,

3

Freddo!! Ho bisogno di un po 'di codice come questo, ma per confrontare i percorsi delle directory su macchine Linux. Ho scoperto che questo non funzionava in situazioni in cui una directory principale era la destinazione.

Ecco una versione del metodo intuitiva della directory:

 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

Suppongo che tu abbia fromPath (un percorso assoluto per una cartella) e toPath (un percorso assoluto per una cartella / file) e stai cercando un percorso che rappresenti il ​​file / cartella in toPath come percorso relativo da fromPath (l'attuale directory di lavoro è fromPath ), quindi qualcosa del genere dovrebbe funzionare:

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

Molte risposte già qui, ma ho scoperto che non gestivano tutti i casi, come la base e l'obiettivo erano gli stessi. Questa funzione accetta una directory di base e un percorso target e restituisce il percorso relativo. Se non esiste un percorso relativo, viene restituito il percorso target. File.separator non è necessario.

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

Ecco un metodo che risolve un percorso relativo da un percorso di base indipendentemente dal fatto che siano nella stessa o in una radice diversa:

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

0

Supera i test di Dónal, l'unica modifica - se nessuna radice comune restituisce il percorso target (potrebbe essere già relativo)

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

Se stai scrivendo un plugin Maven, puoi usare Plexus 'PathTool :

import org.codehaus.plexus.util.PathTool;

String relativeFilePath = PathTool.getRelativeFilePath(file1, file2);

0

Se Paths non è disponibile per il runtime JRE 1.5 o il plug-in 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();
    }

}


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

-2

Pseudo-codice:

  1. Dividi le stringhe per il separatore di tracciato ("/")
  2. Trova il più grande percorso comune ripetendo il risultato della stringa divisa (in modo da finire con "/ var / data" o "/ a" nei tuoi due esempi)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);

2
Questa risposta è al massimo un trucco. Che dire di Windows?
Qix - MONICA È STATA MISTREATA il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.