blob: a92d7e488d2d8c959298f9891716fd71705a43f4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.tools.ant.types.selectors;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.util.FileUtils;
/**
* Container for a path that has been split into its components.
* @since 1.8.0
*/
public class TokenizedPath {
/**
* Instance that holds no tokens at all.
*/
public static final TokenizedPath EMPTY_PATH =
new TokenizedPath("", new String[0]);
/** Helper. */
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
/** iterations for case-sensitive scanning. */
private static final boolean[] CS_SCAN_ONLY = new boolean[] {true};
/** iterations for non-case-sensitive scanning. */
private static final boolean[] CS_THEN_NON_CS = new boolean[] {true, false};
private final String path;
private final String[] tokenizedPath;
/**
* Initialize the TokenizedPath by parsing it.
* @param path The path to tokenize. Must not be
* <code>null</code>.
*/
public TokenizedPath(String path) {
this(path, SelectorUtils.tokenizePathAsArray(path));
}
/**
* Creates a new path as a child of another path.
*
* @param parent the parent path
* @param child the child, must not contain the file separator
*/
public TokenizedPath(TokenizedPath parent, String child) {
if (!parent.path.isEmpty()
&& parent.path.charAt(parent.path.length() - 1) != File.separatorChar) {
path = parent.path + File.separatorChar + child;
} else {
path = parent.path + child;
}
tokenizedPath = new String[parent.tokenizedPath.length + 1];
System.arraycopy(parent.tokenizedPath, 0, tokenizedPath, 0,
parent.tokenizedPath.length);
tokenizedPath[parent.tokenizedPath.length] = child;
}
/* package */
TokenizedPath(String path, String[] tokens) {
this.path = path;
this.tokenizedPath = tokens;
}
/**
* @return The original path String
*/
@Override
public String toString() {
return path;
}
/**
* The depth (or length) of a path.
* @return int
*/
public int depth() {
return tokenizedPath.length;
}
/* package */
String[] getTokens() {
return tokenizedPath;
}
/**
* From <code>base</code> traverse the filesystem in order to find
* a file that matches the given name.
*
* @param base base File (dir).
* @param cs whether to scan case-sensitively.
* @return File object that points to the file in question or null.
*/
public File findFile(File base, final boolean cs) {
String[] tokens = tokenizedPath;
if (FileUtils.isAbsolutePath(path)) {
if (base == null) {
String[] s = FILE_UTILS.dissect(path);
base = new File(s[0]);
tokens = SelectorUtils.tokenizePathAsArray(s[1]);
} else {
File f = FILE_UTILS.normalize(path);
String s = FILE_UTILS.removeLeadingPath(base, f);
if (s.equals(f.getAbsolutePath())) {
//removing base from path yields no change; path
//not child of base
return null;
}
tokens = SelectorUtils.tokenizePathAsArray(s);
}
}
return findFile(base, tokens, cs);
}
/**
* Do we have to traverse a symlink when trying to reach path from
* basedir?
* @param base base File (dir).
* @return boolean
*/
public boolean isSymlink(File base) {
for (String token : tokenizedPath) {
final Path pathToTraverse;
if (base == null) {
pathToTraverse = Paths.get(token);
} else {
pathToTraverse = Paths.get(base.toPath().toString(), token);
}
if (Files.isSymbolicLink(pathToTraverse)) {
return true;
}
base = new File(base, token);
}
return false;
}
/**
* true if the original paths are equal.
* @return boolean
*/
@Override
public boolean equals(Object o) {
return o instanceof TokenizedPath
&& path.equals(((TokenizedPath) o).path);
}
@Override
public int hashCode() {
return path.hashCode();
}
/**
* From <code>base</code> traverse the filesystem in order to find
* a file that matches the given stack of names.
*
* @param base base File (dir) - must not be null.
* @param pathElements array of path elements (dirs...file).
* @param cs whether to scan case-sensitively.
* @return File object that points to the file in question or null.
*/
private static File findFile(File base, final String[] pathElements,
final boolean cs) {
for (String pathElement : pathElements) {
if (!base.isDirectory()) {
return null;
}
String[] files = base.list();
if (files == null) {
throw new BuildException("IO error scanning directory %s",
base.getAbsolutePath());
}
boolean found = false;
boolean[] matchCase = cs ? CS_SCAN_ONLY : CS_THEN_NON_CS;
for (int i = 0; !found && i < matchCase.length; i++) {
for (int j = 0; !found && j < files.length; j++) {
if (matchCase[i]
? files[j].equals(pathElement)
: files[j].equalsIgnoreCase(pathElement)) {
base = new File(base, files[j]);
found = true;
}
}
}
if (!found) {
return null;
}
}
return pathElements.length == 0 && !base.isDirectory() ? null : base;
}
/**
* Creates a TokenizedPattern from the same tokens that make up
* this path.
*
* @return TokenizedPattern
*/
public TokenizedPattern toPattern() {
return new TokenizedPattern(path, tokenizedPath);
}
}