com.teamscale.commons.TeamscaleInstallationUtils Maven / Gradle / Ivy
package com.teamscale.commons;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.engine.core.configuration.EFeatureToggle;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.collections.UnmodifiableSet;
import org.conqat.lib.commons.string.StringUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
/**
* Utilities for retrieving and resolving file paths of a Teamscale installation, e.g. for
* configuration files. The main aim of this class is to ensure file-locating precedence. So
* anything that is user-configurable should be checked first, then the working directory and as
* last resort the installation directory.
*
* Terms in this class are defined as follows:
*
Working directory: The working directory of the Java process. Usually this is the directory
* where Teamscale is started from. It is expected that this directory is writable.
* Installation directory: The directory Teamscale is installed in. This is only available for
* production distributions and not available during development.
* Root directories: Directories that contain Teamscale relevant data. These are the working
* directory and, if set, the installation directory.
* Config directories: Directories that may contain Teamscale relevant configuration. These are
* a user configurable directory via {@value #TEAMSCALE_CONFIG_ENV_VAR} environment variable, and a
* config subdirectory within any of the root directories.
*
* All methods in this class that aim to locate a file or path check for its existence and return an
* empty {@link Optional} if the file/path could not be located.
*/
public final class TeamscaleInstallationUtils {
/** @see #getInstallationDirectory() */
private static final String TEAMSCALE_HOME_ENV_VAR = "TEAMSCALE_HOME";
/**
* Environment variable name that indicates the Teamscale configuration directory. Setting this
* environment variable is optional.
*/
private static final String TEAMSCALE_CONFIG_ENV_VAR = "TEAMSCALE_CONFIG";
/**
* The name of the config dir that is resolved relative to the process working directory or
* Teamscale installation directory.
*/
private static final String CONFIG_DIR_NAME = "config";
private static final Path WORKING_DIRECTORY = detectWorkingDirectory();
private static final @Nullable Path INSTALLATION_DIRECTORY = detectInstallationDirectory();
private static final ImmutableList ROOT_DIRECTORIES = detectRootDirectories(WORKING_DIRECTORY,
INSTALLATION_DIRECTORY);
private static final ImmutableList CONFIG_DIRECTORIES = detectConfigDirectories(ROOT_DIRECTORIES);
// LOGGER must be initialized last, as the LogManager initialization requires
// the loading of config files, which can only be loaded if the statements above
// were executed.
// Only happens when this class is the first one, which uses LogManager.
// Unlikely, but not impossible (e.g., during single Unit test execution).
private static final Logger LOGGER = LogManager.getLogger();
private TeamscaleInstallationUtils() {
// make class non-instantiable
}
private static Path detectWorkingDirectory() {
return Paths.get(System.getProperty("user.dir"));
}
private static Path detectInstallationDirectory() {
String teamscaleHome = System.getenv(TEAMSCALE_HOME_ENV_VAR);
if (!StringUtils.isEmpty(teamscaleHome)) {
return Paths.get(teamscaleHome);
}
return null;
}
/**
* Detects the Teamscale root directories.
*
* @see TeamscaleInstallationUtils
*/
@SuppressWarnings("SameParameterValue" /* We pass variables to ensure initialization order. */)
private static ImmutableList detectRootDirectories(Path workingDirectory, Path installationDirectory) {
List rootDirs = new ArrayList<>();
rootDirs.add(workingDirectory);
if (installationDirectory != null) {
rootDirs.add(installationDirectory);
}
return ImmutableList.copyOf(rootDirs);
}
/**
* Detects the Teamscale configuration directories.
*
* @see TeamscaleInstallationUtils
*/
@SuppressWarnings("SameParameterValue" /* We pass variables to ensure initialization order. */)
private static ImmutableList detectConfigDirectories(ImmutableList rootDirectories) {
List configDirs = new ArrayList<>();
String customConfigDir = System.getenv(TEAMSCALE_CONFIG_ENV_VAR);
if (!StringUtils.isEmpty(customConfigDir)) {
configDirs.add(Paths.get(customConfigDir));
}
rootDirectories.forEach(dir -> configDirs.add(dir.resolve(CONFIG_DIR_NAME)));
return configDirs.stream().filter(Files::isDirectory).map(Path::normalize).distinct()
.collect(ImmutableList.toImmutableList());
}
/**
* Returns the path to the directory of the Teamscale installation. An empty optional is returned if
* the environment variable {@value #TEAMSCALE_HOME_ENV_VAR} is not set. This variable is usually
* set by Teamscale startup scripts, but unset in development mode. It is not expected that the user
* sets this variable manually.
*/
public static Optional getInstallationDirectory() {
return Optional.ofNullable(INSTALLATION_DIRECTORY);
}
/**
* Attempts to locate a file as absolute path, relative to the working directory or relative to the
* Teamscale installation directory. Returns an empty optional if not existing or not readable.
*
* @see TeamscaleInstallationUtils
*/
public static Optional locateRootPath(String name) {
return Streams.concat(Stream.of(Paths.get(name)), ROOT_DIRECTORIES.stream().map(dir -> dir.resolve(name)))
.filter(Files::isReadable).findFirst();
}
/**
* Similar to {@link #locateRootPath(String)}, but returns the absolute path (= provided name) if
* the path cannot be resolved .
*
* @see TeamscaleInstallationUtils
*/
public static Path getRootPath(String name) {
return locateRootPath(name).orElse(Paths.get(name));
}
/**
* Returns all existing directories with the given {@code name} in the current working directory,
* and within all directories within the "server" folder of Teamscale's own git repository.
*
* Make sure this method is only called, iff {@link EFeatureToggle#ENABLE_DEV_MODE} is enabled, and
* the working directory is within the teamscale git repository, i.e., only during actual Teamscale
* development.
*/
public static UnmodifiableSet getRepositoryDirectories(String name) {
Set result = new HashSet<>();
Path rootPath = Paths.get(name).toAbsolutePath();
if (Files.isDirectory(rootPath)) {
// Always support the configured directory if it exists
result.add(rootPath.toAbsolutePath());
}
Path serverPath = getTeamscaleRepositoryRootPath(rootPath).resolve("server");
CCSMAssert.isTrue(Files.isDirectory(serverPath),
() -> String.format("Expected to find a \"server\" directory within the teamscale repository: %s",
serverPath.getParent().toAbsolutePath()));
try {
Files.walkFileTree(serverPath, new SimpleFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.equals(serverPath) || dir.getParent().equals(serverPath)) {
return FileVisitResult.CONTINUE;
}
if (dir.endsWith(name)) {
result.add(dir.toAbsolutePath());
// Already found the directory, no need to look into sibling folders
return FileVisitResult.SKIP_SIBLINGS;
}
// dir is a directory within a module in the "server" folder e.g.
// engine/com.teamscale.service/src
// No need to further look into children as we are only interested in folders
// directly within the module
return FileVisitResult.SKIP_SUBTREE;
}
});
} catch (IOException e) {
LOGGER.error("Unable to detect all check-descriptions paths", e);
}
return CollectionUtils.asUnmodifiable(result);
}
private static Path getTeamscaleRepositoryRootPath(Path rootPath) {
CCSMAssert.isTrue(EFeatureToggle.ENABLE_DEV_MODE.isEnabled(),
() -> String.format("Expected feature toggle \"%s\" to be enabled", EFeatureToggle.ENABLE_DEV_MODE));
Path currentPath = rootPath;
// Detect the teamscale root directory by looking for the "gradlew.bat" file,
// which is only present at the repository root
while (currentPath != null && !Files.exists(currentPath.resolve("gradlew.bat"))) {
currentPath = currentPath.getParent();
}
return Objects.requireNonNull(currentPath,
() -> String.format("Could not find \"teamscale\" repository root directory within: %s", rootPath));
}
/**
* Attempts to locate a file relative to the available configuration directories. Returns the first
* existing and readable configuration file, or an empty optional of none is found.
*
* @see TeamscaleInstallationUtils
*/
public static Optional locateConfigFile(String filename) {
return CONFIG_DIRECTORIES.stream().map(configDir -> configDir.resolve(filename).toFile()).filter(File::canRead)
.findFirst();
}
/**
* Returns the config directories used by Teamscale.
*
* @see #detectConfigDirectories(ImmutableList)
*/
public static List getConfigDirectories() {
return CONFIG_DIRECTORIES;
}
/**
* Lists all readable config files from the config directories. The returned stream starts with
* listing files from the most relevant configuration directory and ends with the least relevant
* one.
*
* @see TeamscaleInstallationUtils
*/
public static Stream listConfigFiles() {
return CONFIG_DIRECTORIES.stream().flatMap(TeamscaleInstallationUtils::listReadableFiles).map(Path::toFile);
}
private static Stream listReadableFiles(Path directory) {
try {
return Files.list(directory).filter(Files::isReadable).filter(Files::isRegularFile);
} catch (IOException e) {
// Swallow as we're just interested in readable files.
return Stream.empty();
}
}
}