Given two absolute paths, e.g.
/var/data/stuff/xyz.dat
/var/data
How can one create a relative path that uses the second path as its base? In the example above, the result should be: ./stuff/xyz.dat
Given two absolute paths, e.g.
/var/data/stuff/xyz.dat
/var/data
How can one create a relative path that uses the second path as its base? In the example above, the result should be: ./stuff/xyz.dat
It's a little roundabout, but why not use URI? It has a relativize method which does all the necessary checks for you.
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"
Please note that for file path there's java.nio.file.Path#relativize since Java 1.7, as pointed out by @Jirka Meluzin in the other answer.
When using java.net.URI.relativize you should be aware of Java bug: JDK-6226081 (URI should be able to relativize paths with partial roots)
At the moment, the
relativize()method ofURIwill only relativize URIs when one is a prefix of the other.
Which essentially means java.net.URI.relativize will not create ".."'s for you.
If you know the second string is part of the first:
String s1 = "/var/data/stuff/xyz.dat";
String s2 = "/var/data";
String s3 = s1.substring(s2.length());
or if you really want the period at the beginning as in your example:
String s3 = ".".concat(s1.substring(s2.length()));
My version is loosely based on Matt and Steve's versions:
/**
* 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());
}
Actually my other answer didn't work if the target path wasn't a child of the base path.
This should work.
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/", "/"));
}
}
Cool!! I need a bit of code like this but for comparing directory paths on Linux machines. I found that this wasn't working in situations where a parent directory was the target.
Here is a directory friendly version of the method:
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;
}
I'm assuming you have fromPath (an absolute path for a folder), and toPath (an absolute path for a folder/file), and your're looking for a path that with represent the file/folder in toPath as a relative path from fromPath (your current working directory is fromPath) then something like this should work:
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();
}
If Paths is not available for JRE 1.5 runtime or maven plugin
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();
}
}
I know this is a bit late but, I created a solution that works with any java version.
public static String getRealtivePath(File root, File file)
{
String path = file.getPath();
String rootPath = root.getPath();
boolean plus1 = path.contains(File.separator);
return path.substring(path.indexOf(rootPath) + rootPath.length() + (plus1 ? 1 : 0));
}
Psuedo-code:
return "." + whicheverPathIsLonger.substring(commonPath.length);