org.conqat.lib.commons.uniformpath.UniformPath 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.lib.commons.uniformpath;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.collections.Pair;
import org.conqat.lib.commons.collections.UnmodifiableMap;
import org.conqat.lib.commons.js_export.ExportToTypeScript;
import org.conqat.lib.commons.net.UrlUtils;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.Preconditions;
/**
* An absolute uniform path with a specific type, where type might be something like "code",
* "non-code" or "architecture" (see {@link UniformPath.EType}). Never includes a project or
* repository name.
*
* Use {@link UniformPathCompatibilityUtil} to create {@link UniformPath} instances from
* {@link String} representations.
*/
@IndexValueClass
public final class UniformPath implements Comparable, Serializable {
private static final long serialVersionUID = 1L;
/** Error message thrown for null parameter. */
/* package */ static final String SEGMENTS_LIST_MAY_NOT_BE_NULL = "Segments list may not be null";
/** The segments indicating an invalid relative path. */
private static final Set RELATIVE_SEGMENTS = new HashSet<>(Arrays.asList(".", ".."));
private static final UnmodifiableMap ESCAPE_MAP = CollectionUtils
.asMap(Pair.createPair("/", "\\/"), Pair.createPair("\\", "\\\\"));
/** The segment inserted between source files and architecture files */
private static final String COMPONENT_TO_FILE_SEGMENT = "-src-";
/** The type of this uniform path. */
/* package */ final EType type;
/**
* The constituent segments of this uniform path, e.g. for the path {@code src/main} this would be
* the array {@code [src, main]}. A segment will never be a relative path (e.g. "." or "..") or
* contain unescaped forward slashes.
*/
private final String[] segments;
private UniformPath(EType pathType, String... pathSegments) {
this.type = pathType;
this.segments = Arrays.copyOf(pathSegments, pathSegments.length);
}
private UniformPath(EType pathType, List pathSegments) {
this.type = pathType;
this.segments = pathSegments.toArray(new String[0]);
}
/**
* Builds a path from the given segments. The segments must consist of non-empty strings that are
* not just relative paths (e.g. "." or "..") and may not contain unescaped slashes ("/").
*
* @see #of(List)
*/
public static UniformPath of(String... segments) {
Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL);
return of(Arrays.asList(segments));
}
/**
* Builds a path from the given segments. The segments must consist of non-empty strings that are
* not just relative paths (e.g. "." or "..") and may not contain unescaped slashes ("/").
*
* @see #of(List)
*/
public static UniformPath of(EType type, String... segments) {
Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL);
return of(type, Arrays.asList(segments));
}
/**
* Builds a path from the given segments. The segments must consist of non-empty strings that are
* not just relative paths (e.g. "." or "..") and may not contain unescaped slashes ("/").
*
* The result is ensured to be of the given type so the according prefix may be present, but does
* not have to. If the uniform path is of a different type an assertion error is thrown.
*
* @see #of(List)
*/
public static UniformPath of(EType type, List segments) {
checkSegmentsValidity(segments);
checkDoesNotStartWithProjectOrRepositoryName(segments);
if (segments.isEmpty()) {
return new UniformPath(type);
}
Optional pathType = EType.parse(segments.get(0));
if (pathType.isPresent()) {
if (pathType.get() != type) {
throw new IllegalArgumentException(
"Uniform path of type " + type + " did start with " + segments.get(0));
}
return new UniformPath(pathType.get(), segments.subList(1, segments.size()));
}
return new UniformPath(type, segments);
}
/**
* Builds a path from the given segments. The segments must consist of non-empty strings that are
* not just relative paths (e.g. "." or "..") and may not contain unescaped slashes ("/").
*/
public static UniformPath of(List segments) {
checkSegmentsValidity(segments);
checkDoesNotStartWithProjectOrRepositoryName(segments);
if (segments.isEmpty()) {
return new UniformPath(EType.CODE);
}
Optional pathType = EType.parse(segments.get(0));
if (pathType.isPresent()) {
return new UniformPath(pathType.get(), segments.subList(1, segments.size()));
}
return new UniformPath(EType.CODE, segments);
}
/**
* Deserializes a uniform path from a string.
*/
@JsonCreator
/* package */ static @Nullable UniformPath getInstance(String uniformPath) {
if (uniformPath == null) {
return null;
}
return UniformPathCompatibilityUtil.convert(uniformPath);
}
private static void checkSegmentsValidity(List segments) {
Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL);
for (String segment : segments) {
checkSegmentValidity(segment, segments, StringUtils::isEmpty, "empty segment");
checkSegmentValidity(segment, segments, RELATIVE_SEGMENTS::contains, "relative segment");
checkSegmentValidity(segment, segments, UniformPath::containsUnescapedSlash, "contains unescaped slash");
}
}
/**
* Checks whether the given string contains an unescaped slash. Escaping is done using a backslash.
*/
/* package */ static boolean containsUnescapedSlash(String data) {
int indexOfLastSlash = data.indexOf('/');
while (indexOfLastSlash != -1) {
if (indexOfLastSlash == 0 || data.charAt(indexOfLastSlash - 1) != '\\') {
return true;
}
indexOfLastSlash = data.indexOf('/', indexOfLastSlash + 1);
}
return false;
}
/**
* Throws an {@link IllegalArgumentException} if the given segments start with a project or
* repository name.
*/
private static void checkDoesNotStartWithProjectOrRepositoryName(List segments) throws AssertionError {
if (segments.size() > 1 && EType.parse(segments.get(1)).isPresent()) {
String pathWithErrorHighlighting = "[" + segments.get(0) + "]/"
+ StringUtils.concat(segments.subList(1, segments.size()), "/");
throw new IllegalArgumentException(
"Invalid path (includes project or repository information): " + pathWithErrorHighlighting);
}
}
/**
* Throws an {@link IllegalArgumentException} containing the given error message if the given check
* detects that the given segment is invalid.
*/
/* package */ static void checkSegmentValidity(String segment, List segments, Predicate errorCheck,
String errorDetailMessage) {
if (errorCheck.test(segment)) {
List pathSegmentsWithErrorHighlighting = CollectionUtils.map(segments, pathSegment -> {
if (errorCheck.test(pathSegment)) {
return "[" + pathSegment + "]";
}
return pathSegment;
});
throw new IllegalArgumentException(String.format("Invalid path (%s): %s", errorDetailMessage,
String.join("/", pathSegmentsWithErrorHighlighting)));
}
}
/** Returns the code type root path. */
public static UniformPath codeRoot() {
return new UniformPath(EType.CODE);
}
/** Returns the test implementation type root path. */
public static UniformPath testImplementationRoot() {
return new UniformPath(EType.TEST_IMPLEMENTATION);
}
/** Returns the test execution type root path. */
public static UniformPath testExecutionRoot() {
return new UniformPath(EType.TEST_EXECUTION);
}
/** Returns the execution unit type root path. */
public static UniformPath executableUnitRoot() {
return new UniformPath(EType.EXECUTION_UNIT);
}
/** Returns the non-code type root path. */
public static UniformPath nonCodeRoot() {
return new UniformPath(EType.NON_CODE);
}
/** Returns the spec-item query root path. */
public static UniformPath specItemQueryRoot() {
return new UniformPath(EType.SPEC_ITEM_QUERY);
}
/**
* Escapes uniform path separators in a segment. The escaping of . and .. is needed because our
* uniform path normalization would resolve them i.e. a/b/../c -> a, which is not desired if the
* segment was explicitly specified as such.
*/
public static String escapeSegment(String segment) {
if (".".equals(segment)) {
return "\\.";
} else if ("..".equals(segment)) {
return "\\.\\.";
}
return StringUtils.escapeChars(segment, ESCAPE_MAP);
}
/** Unescapes uniform path separators in a segment. */
public static String unescapeSegment(String segment) {
if ("\\.".equals(segment)) {
return ".";
} else if ("\\.\\.".equals(segment)) {
return "..";
}
return StringUtils.unescapeChars(segment, ESCAPE_MAP);
}
/**
* Returns the parent of this path, i.e. a new uniform path without the last segment of this path.
*/
public UniformPath getParent() {
Preconditions.checkState(segments.length != 0, "Cannot get the parent of the root path");
return new UniformPath(type, Arrays.asList(segments).subList(0, segments.length - 1));
}
/** Returns the name of the last segment (directory or file) of this path. */
public String getLastSegment() {
Preconditions.checkState(segments.length != 0, "Cannot get the last segment of the root path");
return segments[segments.length - 1];
}
/**
* Returns the sub path relative to the given number of top level path segments. In other words:
* Remove the given number of top level elements from the path.
*
* Example: Removing two segments from {@code src/main/java/Class.java} will yield the relative path
* {@code java/Class.java}.
*
*/
public RelativeUniformPath getSubPath(int numberOfTopLevelSegmentsToRemove) {
Preconditions.checkArgument(numberOfTopLevelSegmentsToRemove <= segments.length,
"Cannot remove more segments than are contained in this path: %s segments to remove, path is %s",
numberOfTopLevelSegmentsToRemove, this);
return RelativeUniformPath
.of(Arrays.asList(segments).subList(numberOfTopLevelSegmentsToRemove, segments.length));
}
/**
* Returns the relative sub path after the given segment.
*
* Example: The subpath after {@code java} in {@code src/main/java/Class.java} will yield the
* relative path {@code Class.java}.
*
*/
public RelativeUniformPath getSubPathAfter(String segment) {
return RelativeUniformPath
.of(Arrays.asList(segments).subList(Arrays.asList(segments).indexOf(segment) + 1, segments.length));
}
/** Checks whether the given uniform path is valid or not. */
public static boolean isValidPath(String uniformPath) {
if (uniformPath == null) {
return false;
}
List segments = UniformPathCompatibilityUtil.getAbsoluteSegments(uniformPath);
try {
checkSegmentsValidity(segments);
checkDoesNotStartWithProjectOrRepositoryName(segments);
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Resolves architecture paths to code paths by omitting the architecture-specific path segments.
*/
public UniformPath resolveToCodePath() {
if (!isArchitecturePath()) {
return this;
}
return UniformPath.codeRoot().resolve(getSubPathAfter(COMPONENT_TO_FILE_SEGMENT));
}
/**
* Returns whether this is a root path. Note that there can be multiple root paths, depending on the
* type (i.e. "-architecture-" and "-non-code-" are both root paths. The path "/" is expanded to
* "-code-" and is also a root path).
*/
public boolean isRoot() {
return segments.length == 0;
}
/** Returns whether this path is a (regular) code path */
public boolean isCodePath() {
return type == EType.CODE;
}
/** Returns whether this path is a non-code path */
public boolean isNonCodePath() {
return type == EType.NON_CODE;
}
/** Returns whether this path is an architecture path */
public boolean isArchitecturePath() {
return type == EType.ARCHITECTURE;
}
/** Returns whether this path is an {@link EType#ISSUE_ITEM issue item} path */
public boolean isIssueItemPath() {
return type == EType.ISSUE_ITEM;
}
/** Returns whether this path is an issue query path */
public boolean isIssueQueryPath() {
return type == EType.ISSUE_QUERY;
}
/** Returns whether this path is a {@link EType#SPEC_ITEM spec item} path */
public boolean isSpecItemPath() {
return type == EType.SPEC_ITEM;
}
/** Returns whether this path is a spec item query path. */
public boolean isSpecItemQueryPath() {
return type == EType.SPEC_ITEM_QUERY;
}
/** Returns whether this path is a test implementation path */
public boolean isTestImplementationPath() {
return type == EType.TEST_IMPLEMENTATION;
}
/** Returns whether this path is a test execution path */
public boolean isTestExecutionPath() {
return type == EType.TEST_EXECUTION;
}
/** Returns whether this path is a test query path */
public boolean isTestQueryPath() {
return type == EType.TEST_QUERY;
}
/** Returns whether this path is a execution unit path */
public boolean isExecutionUnit() {
return type == EType.EXECUTION_UNIT;
}
/** @see #type */
public EType getType() {
return type;
}
/**
* Returns the segments of this uniform path. This method does not return a potential artificial
* first segment. To recreate the exact same {@link UniformPath} object of myPath
, use
* UniformPath.of(myPath.getType(), myPath.getPathSegments())
.
*/
public List getPathSegments() {
return Arrays.asList(this.segments);
}
/**
* Returns the segments of this uniform path as string representation. This method does not return a
* potential artificial first segment.
*
* @see #getPathSegments()
*/
public String getPathSegmentsString() {
return StringUtils.concat(segments, "/");
}
/**
* Same as {@link #getPathSegmentsString()} but escapes each segment to be used in a URL.
*
* @see #getPathSegmentsString()
*/
public String getUrlEncodedPathSegmentsString() {
return StringUtils.concat(CollectionUtils.map(segments, UrlUtils::encodePathSegment), "/");
}
/**
* Resolves the given relative path against this absolute path, performing path canonicalization in
* the process (i.e. ".." will be resolved to parent segment).
*/
public UniformPath resolve(String relativePath) {
return resolve(RelativeUniformPath.of(relativePath));
}
/**
* Resolves the given relative path against this absolute path, performing path canonicalization in
* the process (i.e. ".." will be resolved to parent segment).
*/
public UniformPath resolve(RelativeUniformPath relativePath) {
List segments = new ArrayList<>(Arrays.asList(this.segments));
segments.addAll(relativePath.getSegments());
return UniformPath.of(type, RelativeUniformPath.resolveRelativeSegments(segments));
}
/**
* Returns whether the ancestorPath contains this path as a subpath. Example: {@code src} is an
* ancestor of {@code src/main}.
*/
public boolean hasAncestor(UniformPath ancestorPath) {
if (type != ancestorPath.type) {
return false;
}
String[] containingPathSegments = ancestorPath.segments;
if (containingPathSegments.length > segments.length) {
return false;
}
return Arrays.asList(segments).subList(0, containingPathSegments.length)
.equals(Arrays.asList(containingPathSegments));
}
/**
* Returns whether this path contains the descendantPath as a subpath. Example: {@code /src/main} is
* a descendant of {@code /src}.
*/
public boolean hasDescendant(UniformPath descendantPath) {
if (type != descendantPath.type) {
return false;
}
String[] descendantPathSegments = descendantPath.segments;
if (descendantPathSegments.length < segments.length) {
return false;
}
return Arrays.asList(segments).equals(Arrays.asList(descendantPathSegments).subList(0, segments.length));
}
@Override
@JsonValue
public String toString() {
if (isCodePath()) {
return getPathSegmentsString();
}
if (isRoot()) {
return type.getPrefix();
}
return type.getPrefix() + "/" + getPathSegmentsString();
}
@Override
public int hashCode() {
int prime = 31;
return prime * (prime + type.hashCode()) + Arrays.hashCode(segments);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof UniformPath)) {
return false;
}
UniformPath other = (UniformPath) obj;
return type == other.type && Arrays.equals(segments, other.segments);
}
@Override
public int compareTo(UniformPath other) {
int compareToResult = type.getPrefix().compareTo(other.type.getPrefix());
if (compareToResult != 0) {
if (isCodePath() || other.isCodePath()) {
return toString().compareTo(other.toString());
}
return compareToResult;
}
for (int i = 0; i < Math.min(segments.length, other.segments.length); i++) {
compareToResult = segments[i].compareTo(other.segments[i]);
if (compareToResult != 0) {
return compareToResult;
}
}
return segments.length - other.segments.length;
}
/**
* Returns this uniform path in url encoding (as needed for safely using it in teamscale URLs). For
* example, '/' is replaced by "%2F", '@' is replaced by "%40", and so on.
*/
public String urlEncode() {
if (isCodePath()) {
return getUrlEncodedPathSegmentsString();
}
if (isRoot()) {
return type.getPrefix();
}
return type.getPrefix() + "/" + getUrlEncodedPathSegmentsString();
}
/** All types a path can have. */
@ExportToTypeScript
@IndexValueClass
public enum EType {
/** Code path (default). */
CODE("-code-"),
/** Non-code path. */
NON_CODE("-non-code-"),
/** Architecture path. */
ARCHITECTURE("-architectures-"),
/** Test implementation path. */
TEST_IMPLEMENTATION("-test-implementation-"),
/** Test execution path. */
TEST_EXECUTION("-test-execution-"),
/** Test metric based on a query. */
TEST_QUERY("-test-query-"),
/** Issue item path. */
ISSUE_ITEM("-issue-item-"),
/** Issue item query path. */
ISSUE_QUERY("-issues-"),
/** Spec item path. */
SPEC_ITEM("-spec-item-"),
/** Spec item query. */
SPEC_ITEM_QUERY("-spec-items-"),
/** Execution unit path. */
EXECUTION_UNIT("-execution-unit-");
/** The prefix used for this path. */
private final String prefix;
EType(String prefix) {
this.prefix = prefix;
}
/** Tries to parse the given type string to a path type. */
public static Optional parse(String typeString) {
for (EType type : EType.values()) {
if (typeString.equals(type.getPrefix())) {
return Optional.of(type);
}
}
return Optional.empty();
}
/** @see #prefix */
public String getPrefix() {
return prefix;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy