Comment construire un chemin relatif en Java à partir de deux chemins absolus (ou URL)?

275

Étant donné deux chemins absolus, par exemple

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

Comment créer un chemin relatif qui utilise le second chemin comme base? Dans l'exemple ci-dessus, le résultat doit être:./stuff/xyz.dat

VoidPointer
la source
3
Pour Java 7 et versions ultérieures, voir la réponse de @ VitaliiFedorenko.
Andy Thomas
1
tl; dr réponse: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (qui, soit dit en passant, semble très bien fonctionner avec par exemple "../" pour moi en Java 8 , alors ...)
Andrew

Réponses:

298

C'est un petit rond-point, mais pourquoi ne pas utiliser l'URI? Il dispose d'une méthode de relativisation qui effectue toutes les vérifications nécessaires pour vous.

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"

Veuillez noter que pour le chemin du fichier, il y a java.nio.file.Path#relativizedepuis Java 1.7, comme l'a souligné @Jirka Meluzin dans l'autre réponse .

Adam Crume
la source
17
Voir la réponse de Peter Mueller. relativize () semble assez cassé pour tous sauf les cas les plus simples.
Dave Ray
11
Oui, cela ne fonctionne que si le chemin de base est un parent du premier chemin. Si vous avez besoin d'un retour en arrière hiérarchique comme "../../relativepath", cela ne fonctionnera pas. J'ai trouvé une solution: mrpmorris.blogspot.com/2007/05/…
Aurelien Ribon
4
Comme @VitaliiFedorenko l'a écrit: utilisez java.nio.file.Path#relativize(Path), cela fonctionne uniquement avec les points doubles parents et tout.
Campa
Envisagez d'utiliser toPath()au lieu de toURI(). Il est parfaitement capable de créer des trucs comme "..\..". Mais soyez conscient de l' java.lang.IllegalArgumentException: 'other' has different rootexception lorsque vous demandez un chemin relatif de "C:\temp"à "D:\temp".
Igor
Cela ne fonctionne pas comme prévu, il renvoie data / stuff / xyz.dat dans mon cas de test.
insouciant
238

Depuis Java 7, vous pouvez utiliser la méthode 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);
    }

}

Production:

stuff/xyz.dat
Vitalii Fedorenko
la source
3
Nice, bref, pas de lib supplémentaire +1. La solution d'Adam Crume (hit 1) ne passe pas mes tests et la réponse suivante (hit2) "The Only 'Working' Solution" ajoute un nouveau pot ET est plus de code que mon implémentation, je le trouve ici après ... mieux que jamais .- )
hokr
1
Mais attention à ce problème .
ben3000
1
Vérifié que cela gère l'ajout ..si nécessaire (c'est le cas).
Owen
Malheureusement, Android n'inclut pas java.nio.file:(
Nathan Osman
1
J'ai trouvé que vous obtenez des résultats étranges si la "pathBase" n'est pas "normalisée" avant de "relativiser". Bien que très bien dans cet exemple, je le ferais pathBase.normalize().relativize(pathAbsolute);en règle générale.
pstanton
77

Au moment de la rédaction (juin 2010), c'était la seule solution qui avait réussi mes tests. Je ne peux pas garantir que cette solution est exempte de bogues, mais elle réussit les cas de test inclus. La méthode et les tests que j'ai écrits dépendent de la FilenameUtilsclasse d' Apache commons IO .

La solution a été testée avec Java 1.4. Si vous utilisez Java 1.5 (ou supérieur), vous devriez envisager de remplacer StringBufferpar StringBuilder(si vous utilisez toujours Java 1.4, vous devriez plutôt envisager un changement d'employeur).

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

Les cas de test que cela réussit sont

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
la source
5
Agréable! Une chose, cependant, il casse si la base et la cible sont identiques - la chaîne commune est faite pour se terminer dans un séparateur, que le chemin cible normalisé n'a pas, donc l'appel de sous-chaîne demande un trop de chiffres. Je pense que je l'ai corrigé en ajoutant ce qui suit avant les deux dernières lignes de la fonction: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Erhannis
4
Dire que c'est la seule solution de travail est trompeur. Les autres réponses fonctionnent mieux (cette réponse se bloque lorsque la base et la cible sont identiques), sont plus simples et ne dépendent pas de commons-io.
NateS
26

Lorsque vous utilisez java.net.URI.relativize, vous devez être conscient du bogue Java: JDK-6226081 (l'URI devrait être en mesure de relativiser les chemins avec des racines partielles)

Pour le moment, la relativize()méthode de URIne relativisera les URI que lorsque l'un est un préfixe de l'autre.

Ce qui signifie essentiellement java.net.URI.relativizequ'il ne créera pas de ".." pour vous.

Christian K.
la source
6
Méchant. Il existe une solution de contournement pour cela, apparemment: stackoverflow.com/questions/204784/…
skaffman
Paths.get (startPath) .relativize (Paths.get (endPath)). ToString‌ () semble fonctionner très bien avec par exemple "../" pour moi en Java 8.
Andrew
@skaffman êtes-vous sûr? Cette réponse fait référence au bogue JDK-6226081, mais URIUtils.resolve()mentionne JDK-4708535. Et à partir du code source, je ne vois rien de lié au backtracking (ie les ..segments). Avez-vous confondu les deux bogues?
Garret Wilson
JDK-6920138 est marqué comme un double de JDK-4708535.
Christian K.
18

Dans Java 7 et versions ultérieures, vous pouvez simplement utiliser (et contrairement à URI, il est sans bug):

Path#relativize(Path)
rmuller
la source
17

Le bogue mentionné dans une autre réponse est résolu par URIUtils dans Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Résout une référence URI contre un URI de base. Contournement du bogue dans java.net.URI ()

skaffman
la source
La méthode de résolution ne génère-t-elle pas un URI absolu à partir d'une base et d'un chemin relatif? Comment cette méthode pourrait-elle aider?
Chase
10

Si vous savez que la deuxième chaîne fait partie de la première:

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

ou si vous voulez vraiment la période au début comme dans votre exemple:

String s3 = ".".concat(s1.substring(s2.length()));
Keeg
la source
3
Chaîne s3 = "." + s1.sous-chaîne (s2.length ()); est un peu plus lisible IMO
Dónal
10

La récursivité produit une solution plus petite. Cela lève une exception si le résultat est impossible (par exemple, un disque Windows différent) ou impossible (la racine n'est que le répertoire commun.)

/**
 * 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.
la source
getCanonicalPath peut être lourd, donc cette solution peut être déconseillée lorsque vous devez traiter des centaines de milliers d'enregistrements. Par exemple, j'ai des fichiers de liste contenant jusqu'à un million d'enregistrements et maintenant je veux les déplacer pour utiliser un chemin relatif pour la portabilité.
user2305886
8

Voici une solution autre librairie gratuite:

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

Les sorties

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

[EDIT] en fait, il sort sur plus .. \ en raison de la source est fichier n'est pas un répertoire. La bonne solution pour mon cas est:

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
la source
6

Ma version est vaguement basée sur les versions de Matt et 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
la source
2
+1 fonctionne pour moi. Seule correction mineure: au lieu de "/".length()vous, utilisez separator.length
leonbloy
5

La solution de Matt B obtient le nombre de répertoires à reculer incorrectement - il devrait être la longueur du chemin de base moins le nombre d'éléments de chemin communs, moins un (pour le dernier élément de chemin, qui est soit un nom de fichier soit un suivi ""généré par split) . Il se trouve que cela fonctionne avec /a/b/c/et /a/x/y/, mais remplacez les arguments par /m/n/o/a/b/c/et/m/n/o/a/x/y/ et vous verrez le problème.

En outre, il a besoin d'un else breakà l'intérieur de la première boucle for, ou il gèrera mal les chemins qui se trouvent avoir des noms de répertoire correspondants, tels que /a/b/c/d/et /x/y/c/z- le cest dans le même emplacement dans les deux tableaux, mais n'est pas une correspondance réelle.

Toutes ces solutions n'ont pas la capacité de gérer des chemins qui ne peuvent pas être relativisés les uns aux autres car ils ont des racines incompatibles, telles que C:\foo\baret D:\baz\quux. Probablement seulement un problème sur Windows, mais mérite d'être noté.

J'ai passé beaucoup plus de temps que prévu à ce sujet, mais ça va. J'avais en fait besoin de cela pour le travail, alors merci à tous ceux qui sont intervenus, et je suis sûr qu'il y aura aussi des corrections à cette version!

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

Et voici des tests pour couvrir plusieurs cas:

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
la source
4

En fait, mon autre réponse n'a pas fonctionné si le chemin cible n'était pas un enfant du chemin de base.

Cela devrait fonctionner.

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

}
mat b
la source
2
Au lieu de File.pathSeparator devrait être File.separator. pathSeparator ne doit utiliser que pour split (regex), comme pour "////" regex (win path regex), le chemin du résultat sera incorrect.
Alex Ivasyuv
3

Cool!! J'ai besoin d'un peu de code comme celui-ci, mais pour comparer les chemins de répertoire sur les machines Linux. J'ai trouvé que cela ne fonctionnait pas dans les situations où un répertoire parent était la cible.

Voici une version de la méthode adaptée aux répertoires:

 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
la source
2

Je suppose que vous avez fromPath (un chemin absolu pour un dossier) et toPath (un chemin absolu pour un dossier / fichier), et que vous recherchez un chemin qui représente le fichier / dossier dans toPath comme chemin relatif de fromPath (votre répertoire de travail actuel est fromPath ) alors quelque chose comme ça devrait fonctionner:

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
la source
1

Beaucoup de réponses sont déjà là, mais j'ai trouvé qu'elles ne traitaient pas tous les cas, comme la base et la cible étant les mêmes. Cette fonction prend un répertoire de base et un chemin cible et renvoie le chemin relatif. Si aucun chemin relatif n'existe, le chemin cible est renvoyé. File.separator n'est pas nécessaire.

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
la source
0

Voici une méthode qui résout un chemin relatif à partir d'un chemin de base, qu'ils soient dans la même racine ou dans une racine différente:

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
la source
0

Réussit les tests de Dónal, le seul changement - s'il n'y a pas de racine commune, il retourne le chemin cible (il pourrait être déjà 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
la source
0

Si vous écrivez un plugin Maven, vous pouvez utiliser PlexusPathTool :

import org.codehaus.plexus.util.PathTool;

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

Si Paths n'est pas disponible pour le runtime JRE 1.5 ou le 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();
    }

}
alftank
la source
-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
la source
-2

Code pseudo:

  1. Séparez les chaînes par le séparateur de chemin ("/")
  2. Trouvez le plus grand chemin commun en itérant à travers le résultat de la chaîne fractionnée (vous vous retrouveriez donc avec "/ var / data" ou "/ a" dans vos deux exemples)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
mat b
la source
2
Cette réponse est au mieux un hack. Et les fenêtres?
Qix - MONICA A ÉTÉ BRUÉE