How to check if a given path is possible child of another path?
I am trying to find if given path is possible child开发者_运维知识库 of another path using java. Both path may not exist.
Say c:\Program Files\My Company\test\My App
is a possible child of c:\Program Files
.
Currently I am doing this with
boolean myCheck(File maybeChild, File possibleParent)
{
return maybeChild.getAbsolutePath().startsWith( possibleParent.getAbsolutePath());
}
You can also use java.nio.file.Path
to do this much more easily. The java.nio.file.Path.startsWith
method seems to handle all possible cases.
Example:
private static void isChild(Path child, String parentText) {
Path parent = Paths.get(parentText).toAbsolutePath();
System.out.println(parentText + " = " + child.startsWith(parent));
}
public static void main(String[] args) {
Path child = Paths.get("/FolderA/FolderB/File").toAbsolutePath();
isChild(child, "/FolderA/FolderB/File");
isChild(child, "/FolderA/FolderB/F");
isChild(child, "/FolderA/FolderB");
isChild(child, "/FolderA/Folder");
isChild(child, "/FolderA");
isChild(child, "/Folder");
isChild(child, "/");
isChild(child, "");
}
Output:
/FolderA/FolderB/File = true
/FolderA/FolderB/F = false
/FolderA/FolderB = true
/FolderA/Folder = false
/FolderA = true
/Folder = false
/ = true
= false
If you need more reliability you can use toRealPath
instead of toAbsolutePath
.
File parent = maybeChild.getParentFile();
while ( parent != null ) {
if ( parent.equals( possibleParent ) )
return true;
parent = parent.getParentFile();
}
return false;
Asides from the fact the paths may not exist (and the canonicalisation may not succeed), this looks like a reasonable approach that should work in the straightforward case.
You may want to look at calling getParentFile() on the "maybe child" in a loop, testing if it matches the parent at each step. You can also short-circuit the comparison if the parent isn't a (real) directory.
Perhaps something like the following:
boolean myCheck(File maybeChild, File possibleParent) throws IOException
{
final File parent = possibleParent.getCanonicalFile();
if (!parent.exists() || !parent.isDirectory()) {
// this cannot possibly be the parent
return false;
}
File child = maybeChild.getCanonicalFile();
while (child != null) {
if (child.equals(parent)) {
return true;
}
child = child.getParentFile();
}
// No match found, and we've hit the root directory
return false;
}
Note that if you want the child relationship to be strict (i.e. a directory is not a child of itself) you can change the initial child
assignment on line 9 to be child.getParentFile()
so the first check happens on the child's containing directory.
This will work for your example. It will also return true
if the child is a relative path
(which is often desirable.)
boolean myCheck(File maybeChild, File possibleParent)
{
URI parentURI = possibleParent.toURI();
URI childURI = maybeChild.toURI();
return !parentURI.relativize(childURI).isAbsolute();
}
That will probably work fine as it is, although I would use getCanonicalPath()
rather than getAbsolutePath()
. This should normalize any weird paths like x/../y/z
which would otherwise screw up the matching.
maybeChild.getCanonicalPath().startsWith( possibleParent.getCanonicalPath() );
Be aware of relative paths! I think that simplest solution is something like this:
public boolean myCheck(File maybeChild, File possibleParent) {
if (requestedFile.isAbsolute) {
return possibleParent.resolve(maybeChild).normalize().toAbsolutePath.startsWith(possibleParent.normalize().toAbsolutePath)
} else {
return maybeChild.normalize().toAbsolutePath.startsWith(possibleParent.normalize().toAbsolutePath)
}
}
In scala you can have similar approach:
val baseDir = Paths.get("/home/luvar/tmp")
val baseDirF = baseDir.toFile
//val requestedFile = Paths.get("file1")
val requestedFile = Paths.get("../.viminfo")
val fileToBeRead = if (requestedFile.isAbsolute) {
requestedFile
} else {
baseDir.resolve(requestedFile)
}
fileToBeRead.toAbsolutePath
baseDir.toAbsolutePath
fileToBeRead.normalize()
baseDir.normalize()
val isSubpath = fileToBeRead.normalize().toAbsolutePath.startsWith(baseDir.normalize().toAbsolutePath)
Old question but a pre-1.7 solution:
public boolean startsWith(String possibleRoot, String possibleChildOrSame) {
String[] possiblePath = new File(possibleRoot).getAbsolutePath().replace('\\', '/').split("/");
String[] possibleChildOrSamePath = new File(possibleChildOrSame).getAbsolutePath().replace('\\', '/').split("/");
if (possibleChildOrSamePath.length < possiblePath.length) {
return false;
}
// not ignoring case
for (int i = 0; i < possiblePath.length; i++) {
if (!possiblePath[i].equals(possibleChildOrSamePath[i])) {
return false;
}
}
return true;
}
For completeness the java 1.7+ solution:
public boolean startsWith(String possibleRoot, String possibleChildOrSame) {
Path p1 = Paths.get(possibleChildOrSame).toAbsolutePath();
Path p2 = Paths.get(possibleRoot).toAbsolutePath();
return p1.startsWith(p2);
}
Surprisingly there is no simple, yet functional solution.
The accepted answer does consider same directories as child, which is wrong.
Here is one using java.nio.file.Path API only:
static boolean isChildPath(Path parent, Path child){
Path pn = parent.normalize();
Path cn = child.normalize();
return cn.getNameCount() > pn.getNameCount() && cn.startsWith(pn);
}
Test cases:
@Test
public void testChildPath() {
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F"))).isFalse();
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/A"))).isTrue();
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/A.txt"))).isTrue();
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/../A"))).isFalse();
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/FA"))).isFalse();
assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA"))).isFalse();
assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA/B"))).isTrue();
assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA/B"))).isTrue();
assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderAB"))).isFalse();
assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/Z/X/../A"))).isTrue();
}
When testing paths for equality, the following considerations should be taken into account:
- Case sensitivity of the file system. The only API which can handle case (in)sensitive file systems is NIO.2 (1.7+), that's why neither
java.io.File
norString
can be used. - Individual path entry handling:
C:\abc
is not an ancestor, nor even an immediate parent ofC:\abcd
, henceString.startsWith()
API can't be used. - On Windows,
C:\Program Files
is the same directory asC:\PROGRA~1
, andFiles.isSameFile()
(from NIO.2) is the only API that can handle this right. This is whatPath.startsWith()
approach doesn't support. - Symlink friendliness (not fully covered by my answer since the actual requirements may differ). For directory symlinks,
Files.isSameFile()
supports this to some extent, so thatC:\Documents and Settings
is indeed an ancestor ofC:\Users\Public
. Again, this is where custom code works slightly better than thePath.startsWith()
API (see this most-voted answer).
Having said the above, the solution may look like this. Java:
boolean isAncestorOf(final Path parent, final Path child) {
final Path absoluteParent = parent.toAbsolutePath().normalize();
final Path absoluteChild = child.toAbsolutePath().normalize();
if (absoluteParent.getNameCount() >= absoluteChild.getNameCount()) {
return false;
}
final Path immediateParent = absoluteChild.getParent();
if (immediateParent == null) {
return false;
}
return isSameFileAs(absoluteParent, immediateParent) || isAncestorOf(absoluteParent, immediateParent);
}
boolean isSameFileAs(final Path path, final Path path2) {
try {
return Files.isSameFile(path, path2);
}
catch (final IOException ioe) {
return path.toAbsolutePath().normalize().equals(path2.toAbsolutePath().normalize());
}
}
Kotlin:
fun Path.isAncestorOf(child: Path): Boolean {
val absoluteParent = toAbsolutePath().normalize()
val absoluteChild = child.toAbsolutePath().normalize()
if (absoluteParent.nameCount >= absoluteChild.nameCount) {
return false
}
val immediateParent = absoluteChild.parent
?: return false
return absoluteParent.isSameFileAs(immediateParent) || absoluteParent.isAncestorOf(immediateParent)
}
fun Path.isSameFileAs(that: Path): Boolean =
try {
Files.isSameFile(this, that)
}
catch (_: NoSuchFileException) {
toAbsolutePath().normalize() == that.toAbsolutePath().normalize()
}
精彩评论