Como construir um caminho relativo em Java a partir de dois caminhos absolutos (ou URLs)?

275

Dados dois caminhos absolutos, por exemplo

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

Como se pode criar um caminho relativo que usa o segundo caminho como base? No exemplo acima, o resultado deve ser:./stuff/xyz.dat

VoidPointer
fonte
3
Para Java 7 e posterior, consulte a resposta de @ VitaliiFedorenko.
Andy Thomas
1
tl; dr resposta: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (que, a propósito, parece estar funcionando muito bem com, por exemplo, "../" para mim no Java 8 , então ...)
Andrew

Respostas:

298

É um pouco indireto, mas por que não usar URI? Ele tem um método de relativização que faz todas as verificações necessárias para você.

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"

Observe que para o caminho do arquivo existe java.nio.file.Path#relativizedesde o Java 1.7, como apontado por @Jirka Meluzin na outra resposta .

Adam Crume
fonte
17
Veja a resposta de Peter Mueller. relativize () parece bastante quebrado para todos, exceto os casos mais simples.
21411 Dave Ray
11
Sim, só funciona se o caminho base for um pai do primeiro caminho. Se você precisar de algum retorno hierárquico como "../../caminho relativo", ele não funcionará. Eu encontrei uma solução: mrpmorris.blogspot.com/2007/05/…
Aurelien Ribon
4
Como o @VitaliiFedorenko escreveu: use java.nio.file.Path#relativize(Path), ele funciona apenas com os pontos duplos dos pais e tudo.
Campa 12/02
Considere usar em toPath()vez de toURI(). É perfeitamente capaz de criar coisas como "..\..". Mas lembre-se da java.lang.IllegalArgumentException: 'other' has different rootexceção ao solicitar o caminho relativo de "C:\temp"para "D:\temp".
Igor
Isso não funciona conforme o esperado, ele retorna data / stuff / xyz.dat no meu caso de teste.
unbekant
238

Desde o Java 7, você pode usar o método 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);
    }

}

Resultado:

stuff/xyz.dat
Vitalii Fedorenko
fonte
3
Bom, curto, sem lib extra +1. A solução de Adam Crume (hit 1) não passa nos meus testes e a próxima resposta (hit2) "The Only 'Working' Solution" adiciona um novo jar E é mais código do que minha implementação, acho isso aqui depois ... melhor do que nunca. )
hokr 14/06
1
Mas cuidado com este problema .
ben3000
1
Verificou se isso lida com a adição sempre ..que necessário (faz).
Owen
Infelizmente, o Android não inclui java.nio.file:(
Nathan Osman
1
Descobri que você obtém resultados estranhos se o "pathBase" não for "normalizado" antes de "relativizar". Embora bem neste exemplo, eu faria pathBase.normalize().relativize(pathAbsolute);como regra geral.
pstanton
77

No momento em que escrevi (junho de 2010), essa era a única solução que passou nos meus casos de teste. Não posso garantir que esta solução esteja livre de erros, mas ela passa nos casos de teste incluídos. O método e os testes que escrevi dependem da FilenameUtilsclasse do IO do Apache commons .

A solução foi testada com Java 1.4. Se você estiver usando o Java 1.5 (ou superior), considere substituir StringBufferpor StringBuilder(se ainda estiver usando o Java 1.4, considere uma mudança de empregador).

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

Os casos de teste que são aprovados são

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
fonte
5
Agradável! Uma coisa, no entanto, é interrompida se a base e o destino forem os mesmos - a string comum é feita para terminar em um separador, que o caminho do destino normalizado não possui, portanto a chamada de substring solicita um número excessivo de dígitos. Acho que eu o corrigi adicionando o seguinte antes das duas últimas linhas da função: if (common.length ()> = normalizedTargetPath.length ()) {return "."; }
Erhannis
4
Dizer que essa é a única solução funcional é enganoso. Outras respostas funcionam melhor (essa resposta falha quando a base e o alvo são os mesmos), são mais simples e não dependem do commons-io.
NateS
26

Ao usar o java.net.URI.relativize, você deve estar ciente do bug do Java: JDK-6226081 (o URI deve poder relativizar caminhos com raízes parciais)

No momento, o relativize()método de URIapenas relativiza os URIs quando um é um prefixo do outro.

O que essencialmente significa java.net.URI.relativizeque não criará ".." para você.

Christian K.
fonte
6
Desagradável. Existe uma solução para isso, aparentemente: stackoverflow.com/questions/204784/...
skaffman
Paths.get (startPath) .relativize (Paths.get (endPath)) toString () parece estar funcionando muito bem com por exemplo, "../" para mim em Java 8..
Andrew
@skaffman você tem certeza? Esta resposta faz referência ao bug JDK-6226081, mas URIUtils.resolve()menciona JDK-4708535. E do código fonte, não vejo nada relacionado ao retorno (ou seja, ..segmentos). Você confundiu os dois erros?
Garret Wilson
JDK-6920138 está marcado como uma duplicata de JDK-4708535.
Christian K.
18

No Java 7 e posterior, você pode simplesmente usar (e, ao contrário URI, é livre de erros):

Path#relativize(Path)
rmuller
fonte
17

O bug referido em outra resposta é solucionado por URIUtils no Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Resolve uma referência de URI em relação a um URI base. Solução alternativa para bug em java.net.URI ()

skaffman
fonte
O método de resolução não gera um URI absoluto a partir de uma base e um caminho relativo? Como esse método ajudaria?
Chase
10

Se você souber que a segunda string faz parte da primeira:

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

ou se você realmente deseja o período no início, como no seu exemplo:

String s3 = ".".concat(s1.substring(s2.length()));
Keeg
fonte
3
String s3 = "." + s1.substring (s2.length ()); é um pouco mais legível IMO
Dónal 15/10/08
10

A recursão produz uma solução menor. Isso gera uma exceção se o resultado for impossível (por exemplo, disco diferente do Windows) ou impraticável (raiz é apenas o diretório comum).

/**
 * 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.
fonte
O getCanonicalPath pode ser pesado, portanto, essa solução não é recomendada quando você precisa processar cem mil registros. Por exemplo, tenho alguns arquivos de listagem com até milhões de registros e agora quero movê-los para usar o caminho relativo para portabilidade.
user2305886
8

Aqui está uma solução gratuita para outras bibliotecas:

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

Saídas

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

[EDIT] na verdade ele gera mais .. \ por causa da origem do arquivo e não do diretório. A solução correta para o meu 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);
Jirka Meluzin
fonte
6

Minha versão é vagamente baseada nas versões de 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());
 }
Gili
fonte
2
+1 funciona para mim. Apenas pequenas correção: em vez de "/".length()você deve usar separator.length
leonbloy
5

A solução de Matt B faz com que o número de diretórios retorne errado - deve ser o comprimento do caminho base menos o número de elementos comuns do caminho, menos um (para o último elemento do caminho, que é um nome de arquivo ou um resultado ""gerado por split) . Acontece que ele trabalha com /a/b/c/e /a/x/y/, mas substitui os argumentos por /m/n/o/a/b/c/e/m/n/o/a/x/y/ e você verá o problema.

Além disso, ele precisa de um else breakdentro do primeiro loop for, ou manipulará incorretamente os caminhos que possuem nomes de diretório correspondentes, como /a/b/c/d/e /x/y/c/z- oc está no mesmo slot nas duas matrizes, mas não é uma correspondência real.

Todas essas soluções não têm a capacidade de lidar com caminhos que não podem ser relativizados entre si porque possuem raízes incompatíveis, como C:\foo\bareD:\baz\quux . Provavelmente apenas um problema no Windows, mas vale a pena notar.

Passei muito mais tempo nisso do que pretendia, mas tudo bem. Na verdade, eu precisava disso para trabalhar, por isso agradeço a todos que participaram, e tenho certeza de que também haverá correções nesta versão!

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 aqui estão os testes para cobrir vários casos:

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

Na verdade, minha outra resposta não funcionaria se o caminho de destino não fosse filho do caminho base.

Isso deve funcionar.

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
fonte
2
Em vez de File.pathSeparator deve ser File.separator. pathSeparator deve usar apenas para divisão (regex), pois para "////" regex (win path regex), o caminho do resultado estará incorreto.
Alex Ivasyuv 29/03/10
3

Legal!! Eu preciso de um pouco de código como este, mas para comparar os caminhos de diretório em máquinas Linux. Descobri que isso não estava funcionando em situações em que um diretório pai era o alvo.

Aqui está uma versão amigável do diretório do método:

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

Suponho que você tenha fromPath (um caminho absoluto para uma pasta) e toPath (um caminho absoluto para uma pasta / arquivo), e você está procurando um caminho que represente o arquivo / pasta no toPath como um caminho relativo from fromPath (seu diretório de trabalho atual é fromPath ), algo assim deve funcionar:

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

Muitas respostas já estão aqui, mas descobri que elas não lidavam com todos os casos, como a base e o destino, sendo os mesmos. Essa função pega um diretório base e um caminho de destino e retorna o caminho relativo. Se não existir um caminho relativo, o caminho de destino será retornado. File.separator é desnecessário.

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

Aqui, um método que resolve um caminho relativo a partir de um caminho base, independentemente de estarem na mesma raiz ou em uma raiz diferente:

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

Passa nos testes de Dónal, a única mudança - se nenhuma raiz comum retorna o caminho de destino (já pode ser 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);
    }
}
Mike
fonte
0

Se você estiver escrevendo um plug-in Maven, poderá usar o Plexus 'PathTool :

import org.codehaus.plexus.util.PathTool;

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

Se Paths não estiver disponível para o tempo de execução do JRE 1.5 ou o 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
fonte
-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
fonte
-2

Código Psuedo:

  1. Divida as seqüências pelo separador de caminho ("/")
  2. Encontre o melhor caminho comum iterando pelo resultado da cadeia de caracteres dividida (para que você acabe com "/ var / data" ou "/ a" em seus dois exemplos)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
matt b
fonte
2
Esta resposta é um hack na melhor das hipóteses. E as janelas?
Qix - MONICA FOI ERRADA