com.nordstrom.common.file.PathUtils Maven / Gradle / Ivy
package com.nordstrom.common.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* This utility class provides a {@link #getNextPath(Path, String, String) getNextPath} method to acquire the next file
* path in sequence for the specified base name and extension in the indicated target folder. If the target folder
* already contains at least one file that matches the specified base name and extension, the algorithm used to select
* the next path will always return a path whose index is one more than the highest index that currently exists. (If a
* single file with no index is found, its implied index is 0.)
*
* Example usage of {@code getNextPath}
*
* ...
*
* /*
* * This example gets the next path in sequence for base name `artifact`
* * and extension `txt` in the TestNG output directory.
* *
* * For purposes of this example, the output directory already contains
* * the following files: `artifact.txt`, `artifact-3.txt`
* */
*
* Path collectionPath = Paths.get(testContext.getOutputDirectory());
* // => C:\git\my-project\test-output\Default suite
*
* Path artifactPath;
* try {
* artifactPath = PathUtils.getNextPath(collectionPath, "artifact", "txt");
* // => C:\git\my-project\test-output\Default suite\artifact-4.txt
* } catch (IOException e) {
* provider.getLogger().info("Unable to get output path; no artifact was captured", e);
* return;
* }
*
* ...
*
*/
public final class PathUtils {
private PathUtils() {
throw new AssertionError("PathUtils is a static utility class that cannot be instantiated");
}
private static final String SUREFIRE_PATH = "surefire-reports";
private static final String FAILSAFE_PATH = "failsafe-reports";
private static final List ENDINGS =
OSInfo.getDefault().getType() == OSInfo.OSType.WINDOWS
? Arrays.asList("", ".cmd", ".exe", ".com", ".bat")
: Collections.singletonList("");
/**
* This enumeration contains methods to help build proxy subclass names and select reports directories.
*/
public enum ReportsDirectory {
SUREFIRE_1("(Test)(.*)", SUREFIRE_PATH),
SUREFIRE_2("(.*)(Test)", SUREFIRE_PATH),
SUREFIRE_3("(.*)(Tests)", SUREFIRE_PATH),
SUREFIRE_4("(.*)(TestCase)", SUREFIRE_PATH),
FAILSAFE_1("(IT)(.*)", FAILSAFE_PATH),
FAILSAFE_2("(.*)(IT)", FAILSAFE_PATH),
FAILSAFE_3("(.*)(ITCase)", FAILSAFE_PATH),
ARTIFACT(".*", "artifact-capture");
private final String regex;
private final String folder;
ReportsDirectory(String regex, String folder) {
this.regex = regex;
this.folder = folder;
}
/**
* Get the regular expression that matches class names for this constant.
*
* @return class-matching regular expression string
*/
public String getRegEx() {
return regex;
}
/**
* Get the name of the folder associated with this constant.
*
* @return class-related folder name
*/
public String getFolder() {
return folder;
}
/**
* Get the resolved Maven-derived path associated with this constant.
*
* @param subdirs optional sub-path
* @return Maven folder path
*/
public Path getPath(String... subdirs) {
return getTargetPath().resolve(Paths.get(folder, subdirs));
}
/**
* Get the reports directory constant for the specified test class object.
*
* @param obj test class object
* @return reports directory constant
*/
public static ReportsDirectory fromObject(Object obj) {
String name = obj.getClass().getSimpleName();
for (ReportsDirectory constant : values()) {
if (name.matches(constant.regex)) {
return constant;
}
}
throw new IllegalStateException("Someone removed the 'default' pattern from this enumeration");
}
/**
* Get reports directory path for the specified test class object.
*
* @param obj test class object
* @return reports directory path
*/
public static Path getPathForObject(Object obj) {
String[] subdirs = {};
if (obj instanceof PathModifier) {
String message = String.format("Null path modifier returned by: %s", obj.getClass().getName());
subdirs = Objects.requireNonNull(((PathModifier) obj).getSubPath(), message);
}
return fromObject(obj).getPath(subdirs);
}
/**
* Get the path for the 'target' folder of the current project.
*
* @return path for project 'target' folder
*/
private static Path getTargetPath() {
return Paths.get(getBaseDir()).resolve("target");
}
}
/**
* Get the next available path in sequence for the specified base name and extension in the specified folder.
*
* @param targetPath path to target directory for the next available path in sequence
* @param baseName base name for the path sequence
* @param extension extension for the path sequence
* @return the next available path in sequence
* @throws IOException if an I/O error is thrown when accessing the starting file.
*/
public static Path getNextPath(Path targetPath, String baseName, String extension) throws IOException {
Objects.requireNonNull(targetPath, "[targetPath] must be non-null");
Objects.requireNonNull(baseName, "[baseName] must be non-null");
Objects.requireNonNull(extension, "[extension] must be non-null");
File targetFile = targetPath.toFile();
if ( ! (targetFile.exists() && targetFile.isDirectory())) {
throw new IllegalArgumentException("[targetPath] must specify an existing directory");
}
if (baseName.isEmpty()) {
throw new IllegalArgumentException("[baseName] must specify a non-empty string");
}
if (extension.isEmpty()) {
throw new IllegalArgumentException("[extension] must specify a non-empty string");
}
Visitor visitor = new Visitor(baseName, extension);
Files.walkFileTree(targetPath, EnumSet.noneOf(FileVisitOption.class), 1, visitor);
return targetPath.resolve(visitor.getNewName());
}
/**
* Get project base directory.
*
* @return project base directory
*/
public static String getBaseDir() {
Path currentRelativePath = Paths.get(System.getProperty("user.dir"));
return currentRelativePath.toAbsolutePath().toString();
}
private static class Visitor implements FileVisitor {
private final String baseName;
private final String extension;
private final int base;
private final int ext;
private final PathMatcher pathMatcher;
private final List intList = new ArrayList<>();
Visitor(String baseName, String extension) {
this.baseName = baseName;
this.extension = extension;
this.base = baseName.length();
this.ext = extension.length() + 1;
this.pathMatcher = FileSystems.getDefault().getPathMatcher("regex:\\Q" + baseName + "\\E(-\\d+)?\\." + extension);
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isRegularFile() && pathMatcher.matches(file.getFileName())) {
String name = file.getFileName().toString();
String iStr = "0" + name.substring(base, name.length() - ext);
iStr = iStr.replace("0-", "");
intList.add(Integer.parseInt(iStr) + 1);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
public String getNewName() {
String newName;
if (intList.isEmpty()) {
newName = baseName + "." + extension;
} else {
intList.sort(Collections.reverseOrder());
newName = baseName + "-" + intList.get(0) + "." + extension;
}
return newName;
}
}
/**
* Search for the specified executable file on the system file path.
*
* NOTE: On Windows, this method automatically checks for files of the specified name/path with
* the standard executable file extensions ({@code .cmd"}, {@code ".exe"}, {@code ".com"},
* and {@code ".bat"}), so these can be omitted for cross-platform compatibility.
*
* @param nameOrPath name/path of executable to find
* @return absolute path of located executable; {@code null} if not found
*/
public static String findExecutableOnSystemPath(final String nameOrPath) {
List paths = getSystemPathList();
paths.add(0, null); // check full path first
for (String path : paths) {
for (String ending : ENDINGS) {
File file = new File(path, nameOrPath + ending);
if (canExecute(file)) {
return file.getAbsolutePath();
}
}
}
return null;
}
/**
* Get the system file path as a path-delimited string.
*
* NOTE: The initial entries in the returned path string are derived from {@link System#getenv()}.
* When running on {@code Mac OS X}, additional entries are acquired from {@code /etc/paths}
* and the files found in the {@code /etc/paths.d} folder.
*
* @return system file path as a path-delimited string
*/
public static String getSystemPath() {
return String.join(File.pathSeparator, getSystemPathList());
}
/**
* Get the system file path as a list of path items.
*
* NOTE: The initial items in the returned path list are derived from {@link System#getenv()}.
* When running on {@code Mac OS X}, additional items are acquired from {@code /etc/paths}
* and the files found in the {@code /etc/paths.d} folder.
*
* @return system file path as a path-delimited string
*/
public static List getSystemPathList() {
List pathList = new ArrayList<>();
addSystemPathList(pathList);
addMacintoshPathList(pathList);
return pathList;
}
/**
* Append the system path entries to the specified list.
*
* NOTE: Added entries are derived from {@link System#getenv()}.
*
* @param pathList existing list to receive system path entries
* @return {@code true} if entries were appended; otherwise {@code false}
*/
public static boolean addSystemPathList(List pathList) {
String name = "PATH";
Map env = System.getenv();
if (!env.containsKey(name)) {
for (String key : env.keySet()) {
if (name.equalsIgnoreCase(key)) {
name = key;
break;
}
}
}
String path = env.get(name);
return (path != null) && addNewPaths(pathList, Arrays.asList(path.split(File.pathSeparator)));
}
/**
* Append Macintosh path entries to the specified list.
*
* NOTE: When running on {@code Mac OS X}, added entries are acquired from {@code /etc/paths}
* and the files found in the {@code /etc/paths.d} folder.
*
* @param pathList existing list to receive Macintosh path entries
* @return {@code true} if entries were appended; otherwise {@code false}
*/
public static boolean addMacintoshPathList(List pathList) {
boolean didChange = false;
if (OSInfo.getDefault().getType() == OSInfo.OSType.MACINTOSH) {
List fileList = new ArrayList<>();
File pathsFile = new File("/etc/paths");
if (pathsFile.exists()) fileList.add(pathsFile);
File[] pathsList = new File("/etc/paths.d").listFiles();
if (pathsList != null) {
fileList.addAll(Arrays.asList(pathsList));
}
for (File thisFile : fileList) {
try {
didChange |= addNewPaths(pathList, Files.readAllLines(thisFile.toPath()));
} catch (IOException eaten) {
// nothing to do here
}
}
}
return didChange;
}
/**
* Append new path entries to the specified list.
*
* NOTE: Entries from [newPaths] that already exist in [pathList] are ignored.
*
* @param pathList existing list to receive new path entries
* @param newPaths path entries to be evaluated for novelty
* @return {@code true} if entries were appended; otherwise {@code false}
*/
private static boolean addNewPaths(List pathList, List newPaths) {
boolean didChange = false;
for (String thisPath : newPaths) {
if (!pathList.contains(thisPath)) {
didChange |= pathList.add(thisPath);
}
}
return didChange;
}
/**
* Prepend the specified string to the indicated array.
*
* @param prefix string to be prepended
* @param strings target string array
* @return target array prefixed with the specified string
*/
public static String[] prepend(String prefix, String... strings) {
int len = strings.length;
String[] temp = new String[len + 1];
if (len > 0) System.arraycopy(strings, 0, temp, 1, len);
temp[0] = prefix;
return temp;
}
/**
* Append the specified string to the indicated array.
*
* @param suffix string to be appended
* @param strings target string array
* @return target array with the specified string appended
*/
public static String[] append(String suffix, String... strings) {
int len = strings.length;
String[] temp = new String[len + 1];
if (len > 0) System.arraycopy(strings, 0, temp, 0, len);
temp[len] = suffix;
return temp;
}
private static boolean canExecute(File file) {
return file.exists() && !file.isDirectory() && file.canExecute();
}
/**
* Classes that implement this interface are called to supply additional elements for the path returned by
* {@link ReportsDirectory#getPathForObject(Object)}. This enables the implementing class to partition artifacts
* based on scenario-specific criteria.
*/
public interface PathModifier {
/**
* Get scenario-specific path modifier for {@link ReportsDirectory#getPathForObject(Object)}.
*
* @return scenario-specific path modifier
*/
public String[] getSubPath();
}
}