org.conqat.engine.resource.util.UniformPathUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of teamscale-commons Show documentation
Show all versions of teamscale-commons Show documentation
Provides common DTOs for Teamscale
The newest version!
/*
* Copyright (c) CQSE GmbH
*
* Licensed 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
*
* http://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.conqat.engine.resource.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.uniformpath.UniformPath;
/**
* Utility methods for dealing with uniform paths.
*/
public class UniformPathUtils {
/**
* Matches windows drive letters prefixes, e.g. "C:/", and captures the path after the drive letter
* as first group. Requires the path to be normalized before matching, as the pattern will not match
* '\'.
*/
public static final Pattern DRIVE_LETTER_PATTERN = Pattern.compile("[A-Za-z]:/(.*)");
/** The character used as path separator in uniform paths. */
public static final char SEPARATOR_CHAR = '/';
/** String representation of {@link #SEPARATOR_CHAR}. */
public static final String SEPARATOR = String.valueOf(SEPARATOR_CHAR);
/** The file extension of a architecture definition file */
public static final String ARCHITECTURE_FILE_EXTENSION = ".architecture";
/**
* Extracts the project part of a uniform path, which is everything up to the first
* {@link #SEPARATOR_CHAR}.
*/
public static String extractProject(String uniformPath) {
return StringUtils.getFirstParts(uniformPath, 1, SEPARATOR_CHAR);
}
/**
* Returns the path without the project, i.e. removes everything up to the first
* {@link #SEPARATOR_CHAR}.
*/
public static String stripProject(String uniformPath) {
int pos = uniformPath.indexOf(SEPARATOR_CHAR);
if (pos >= 0) {
return uniformPath.substring(pos + 1);
}
return uniformPath;
}
/**
* Returns the element name for a uniform path, which is everything starting from the last
* {@link #SEPARATOR_CHAR}.
*/
public static String getElementName(String uniformPath) {
return StringUtils.getLastPart(uniformPath, SEPARATOR_CHAR);
}
/**
* Returns the parent path for a path which is everything up to the last non-escaped
* {@link #SEPARATOR_CHAR}. If no separator is found, the empty string is returned.
*/
public static String getParentPath(String uniformPath) {
// we can not use StringUtils.removeLastPart(), as the behavior for a
// string without separator is different here
int lastSlash = uniformPath.length();
while ((lastSlash = uniformPath.lastIndexOf(SEPARATOR_CHAR, lastSlash - 1)) != -1) {
if (lastSlash > 0 && uniformPath.charAt(lastSlash - 1) != '\\') {
break;
}
}
if (lastSlash == -1) {
return StringUtils.EMPTY_STRING;
}
return uniformPath.substring(0, lastSlash);
}
/** Removes the first count
segments from the given path. */
public static String removeFirstSegments(String uniformPath, int count) {
String[] segments = splitPath(uniformPath);
return concatenate(Arrays.copyOfRange(segments, count, segments.length));
}
/** Removes the last count
segments from the given path. */
public static String removeLastSegments(String uniformPath, int count) {
String[] segments = splitPath(uniformPath);
return concatenate(Arrays.copyOfRange(segments, 0, segments.length - count));
}
/**
* Returns segments forming the given path. This takes escaped slashes into consideration, so
* foo/bar\/great -> ["foo", "bar\/great"]. Also removes leading slashes.
*/
public static String[] splitPath(String uniformPath) {
int currentOffset = -1;
List splitPositions = new ArrayList<>();
while ((currentOffset = uniformPath.indexOf(SEPARATOR_CHAR, currentOffset + 1)) != -1) {
if (currentOffset == 0
|| (currentOffset <= uniformPath.length() - 1 && uniformPath.charAt(currentOffset - 1) != '\\')) {
splitPositions.add(currentOffset);
}
}
if (splitPositions.isEmpty()) {
return new String[] { uniformPath };
}
String[] segments = new String[splitPositions.size() + 1];
for (int i = 0; i < splitPositions.size(); i++) {
if (i == 0) {
segments[i] = uniformPath.substring(0, splitPositions.get(i));
} else {
segments[i] = uniformPath.substring(splitPositions.get(i - 1) + 1, splitPositions.get(i));
}
if (i == splitPositions.size() - 1) {
segments[i + 1] = uniformPath.substring(splitPositions.get(i) + 1);
}
}
return segments;
}
/**
* Returns the extension of the uniform path.
*
* @return File extension, i.e. "java" for "FileSystemUtils.java", or null
, if the path
* has no extension (i.e. if a path contains no '.'), returns the empty string if the '.' is
* the path's last character.
*/
public static @Nullable String getExtension(String uniformPath) {
String name = getElementName(uniformPath);
int posLastDot = name.lastIndexOf('.');
if (posLastDot < 0) {
return null;
}
return name.substring(posLastDot + 1);
}
/** Returns whether the file with the given path is an architecture file */
public static boolean isArchitectureFile(String uniformPath) {
return !uniformPath.startsWith(UniformPath.EType.ARCHITECTURE.getPrefix())
&& ARCHITECTURE_FILE_EXTENSION.equals("." + getExtension(uniformPath));
}
/**
* Returns whether the file with the given path is an architecture root component i.e. a first level
* child of -architectures-.
*/
public static boolean isArchitectureRootComponent(String uniformPath) {
return uniformPath.startsWith(UniformPath.EType.ARCHITECTURE.getPrefix())
&& ARCHITECTURE_FILE_EXTENSION.equals("." + getExtension(uniformPath));
}
/**
* Replaces forward and backward slashes, not only system-specific separators, with a forward slash.
* We do this on purpose, since paths that e.g. are read from files do not necessarily contain the
* separators contained in File.separator. Multiple slashes (regardless of type) in a row are
* collapsed to one. If nothing is changed, the input string is returned.
*
* Implementation Details:
*
* To avoid using regexes for the normalization, for performance reasons, this method uses a lower
* level algorithm, that is much faster (>5x) than the same regex based operation. The idea is that
* we create a output char[] into which we copy all the characters that we want to keep, one by one,
* thus making this operation O(N) and requiring very little allocations. To keep track of where we
* are in the result array, we need to keep a tab on the amount skipped characters, to keep copying
* into the next free slot, after skipping a char. In the end we return a subcopy of the result
* char[], to remove the dangling empty chars in the array, if we removed something.
*
*/
public static String normalizeAllSeparators(String path) {
int pathLength = path.length();
char[] result = new char[pathLength];
int skippedChars = 0;
boolean previousWasPathSeparator = false;
boolean madeChanges = false;
for (int i = 0; i < pathLength; i++) {
char currentChar = path.charAt(i);
switch (currentChar) {
case '\\':
madeChanges = true; // fallthrough intended
case SEPARATOR_CHAR:
if (previousWasPathSeparator) {
// skip the current path separator since it is redundant
madeChanges = true;
skippedChars += 1;
continue;
}
result[i - skippedChars] = SEPARATOR_CHAR;
previousWasPathSeparator = true;
break;
default:
result[i - skippedChars] = currentChar;
previousWasPathSeparator = false;
}
}
if (madeChanges) {
return new String(Arrays.copyOfRange(result, 0, result.length - skippedChars));
} else {
return path;
}
}
/**
* Creates a clean path by resolving duplicate slashes, single and double dots. This is the
* equivalent to path canonization on uniform paths.
*/
public static String cleanPath(String path) {
String[] parts = splitPath(path);
for (int i = 0; i < parts.length; ++i) {
// do not use StringUtils.isEmpty(), as we do not want trim
// semantics!
if (StringUtils.EMPTY_STRING.equals(parts[i]) || ".".equals(parts[i])) {
parts[i] = null;
} else if ("..".equals(parts[i])) {
// cancel last non-null (if any)
int j = i - 1;
for (; j >= 0; --j) {
if ("..".equals(parts[j])) {
// another '..' acts as boundary
break;
}
if (parts[j] != null) {
// cancel both parts of the path.
parts[j] = null;
parts[i] = null;
break;
}
}
}
}
return joinPath(parts);
}
/** Joins the given array as a path, but ignoring null entries. */
private static String joinPath(String[] parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (part != null) {
if (sb.length() > 0) {
sb.append(SEPARATOR_CHAR);
}
sb.append(part);
}
}
return sb.toString();
}
/**
* For a uniform path denoting a file and a relative path, constructs the uniform path for the
* relatively addressed element.
*/
public static String resolveRelativePath(String basePath, String relative) {
// obtain "directory" from path denoting file
String directory = getParentPath(basePath);
if (!directory.isEmpty()) {
directory += SEPARATOR;
}
return cleanPath(directory + relative);
}
/**
* Returns the concatenated path of all given parts. Empty or null strings are ignored.
*/
public static String concatenate(String... parts) {
List list = new ArrayList<>(parts.length);
for (String part : parts) {
if (!StringUtils.isEmpty(part)) {
list.add(part);
}
}
return StringUtils.concat(list, SEPARATOR);
}
/**
* Remove drive letters or unix root slash from path. Also normalizes the path.
*/
public static String createSystemIndependentPath(String path) {
Matcher m1 = DRIVE_LETTER_PATTERN.matcher(normalizeAllSeparators(path));
if (m1.matches()) {
// Remove drive letter
path = m1.group(1);
}
// Remove unix root slash
path = StringUtils.stripPrefix(path, "/");
return path;
}
/**
* Reduces the provided {@code uniformPaths} to their common prefix. All elements must be of the
* same {@link UniformPath#getType() type}.
*/
public static @NonNull UniformPath reduceToCommonPrefix(List uniformPaths) {
return uniformPaths.stream().reduce(UniformPathUtils::getCommonPrefix)
.orElseThrow(() -> new IllegalArgumentException("uniformPaths must not be empty"));
}
/**
* Computes the common prefix of the provided {@link UniformPath}s. Both must be at least of the
* same {@link UniformPath#getType() type}.
*/
public static @NonNull UniformPath getCommonPrefix(UniformPath p1, UniformPath p2) {
if (p1.getType() != p2.getType()) {
throw new IllegalArgumentException(
String.format("Cannot find a common prefix for different uniform path types: %s (%s) and %s (%s)",
p1.getType(), p1, p2.getType(), p2));
}
List segments = new ArrayList<>();
Iterator p1Segments = p1.getPathSegments().iterator();
Iterator p2Segments = p2.getPathSegments().iterator();
while (p1Segments.hasNext() && p2Segments.hasNext()) {
String p1Value = p1Segments.next();
String p2Value = p2Segments.next();
if (Objects.equals(p1Value, p2Value)) {
segments.add(p1Value);
} else {
break;
}
}
return UniformPath.of(p1.getType(), segments);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy