318 votes

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

Étant donné deux chemins absolus, par exemple

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

Comment peut-on créer un chemin relatif qui utilise le deuxième chemin comme base ? Dans l'exemple ci-dessus, le résultat devrait être : ./stuff/xyz.dat

3 votes

Pour Java 7 et les versions ultérieures, voir la réponse de @VitaliiFedorenko.

3 votes

Réponse en résumé : Paths.get(startPath).relativize(Paths.get(endPath)).toString() (ce qui, soit dit en passant, semble fonctionner parfaitement avec par exemple "../" pour moi en Java 8, donc...)

332voto

Adam Crume Points 7444

C'est un peu détourné, mais pourquoi ne pas utiliser l'URI ? Il possède 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#relativize depuis Java 1.7, comme le fait remarquer @Jirka Meluzin en l'autre réponse .

19 votes

Voir la réponse de Peter Mueller. relativize() semble plutôt cassé pour tout sauf les cas les plus simples.

11 votes

Oui, cela ne fonctionne que si le chemin de base est un parent du premier chemin. Si vous avez besoin d'un retour hiérarchique comme "../../relativepath", cela ne fonctionnera pas. J'ai trouvé une solution : mrpmorris.blogspot.com/2007/05/

4 votes

Comme l'a écrit @VitaliiFedorenko : utiliser java.nio.file.Path#relativize(Path) c'est juste que ça marche avec les doubles points des parents et tout.

279voto

Vitalii Fedorenko Points 17469

Depuis Java 7, vous pouvez utiliser l'option relativiser método:

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

}

Sortie :

stuff/xyz.dat

3 votes

Sympathique, court, pas d'extra lib +1. La solution d'Adam Crume (hit 1) ne passe pas mes tests et la réponse suivante (hit 2) "The Only 'Working' Solution" ajoute un nouveau jar ET est plus codée que mon implémentation, je trouve cela ici après... mieux que jamais .-)

1 votes

Mais attention à ce problème .

1 votes

J'ai vérifié que cela gère l'ajout de .. si nécessaire (il le fait).

77voto

Dónal Points 61837

Au moment de la rédaction de ce document (juin 2010), c'était la seule solution qui passait mes tests. Je ne peux pas garantir que cette solution est exempte de bogues, mais elle passe les tests inclus. La méthode et les tests que j'ai rédigés dépendent de l'application FilenameUtils classe de 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 StringBuffer avec StringBuilder (si vous utilisez encore 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
    }
}

5 votes

Joli ! Une chose, cependant, ça casse si la base et la cible sont les mêmes - la chaîne commune est faite pour se terminer par un séparateur, que le chemin cible normalisé n'a pas, donc l'appel de la sous-chaîne demande un chiffre de trop. Je pense avoir résolu le problème en ajoutant ce qui suit avant les deux dernières lignes de la fonction : if (common.length() >= normalizedTargetPath.length()) { return "." ; }

4 votes

Dire que c'est la seule solution qui fonctionne est trompeur. D'autres réponses fonctionnent mieux (cette réponse se plante lorsque la base et la cible sont identiques), sont plus simples et ne dépendent pas de commons-io.

27voto

Christian K. Points 360

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

Pour l'instant, le relativize() méthode de URI ne relativisera les URIs que si l'un est un préfixe de l'autre.

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

6 votes

Méchant. Il existe une solution de contournement pour cela, apparemment : stackoverflow.com/questions/204784/

0 votes

Paths.get(startPath).relativize(Paths.get(endPath)).toString() semble fonctionner parfaitement avec par exemple "../" pour moi en Java 8.

0 votes

@skaffman êtes-vous sûr ? Cette réponse fait référence au bogue JDK-6226081, mais URIUtils.resolve() mentionne JDK-4708535. Et d'après le code source, je ne vois rien concernant le retour en arrière (c.-à-d. .. segments). Avez-vous confondu les deux bugs ?

16voto

skaffman Points 197885

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

public static URI resolve(URI baseURI,
                          String reference)

Résout une référence URI par rapport à un URI de base. Solution de contournement d'un bogue dans java.net.URI ()

0 votes

La méthode resolve ne génère-t-elle pas un URI absolu à partir d'une base et d'un chemin relatif ? En quoi cette méthode serait-elle utile ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X