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
/*-------------------------------------------------------------------------+
| |
| Copyright (c) 2005-2018 The ConQAT Project |
| |
| 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.io.UnsupportedEncodingException;
import java.net.URLEncoder;
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 java.util.regex.Pattern;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.filesystem.FileSystemUtils;
import org.conqat.lib.commons.js_export.ExportToTypeScript;
import org.conqat.lib.commons.string.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
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.
*/
public final class UniformPath implements Comparable, Serializable {
private static final long serialVersionUID = 1L;
/** The name of the JSON property name for {@link #type}. */
protected static final String TYPE_PROPERTY = "type";
/** The name of the JSON property name for {@link #segments}. */
protected static final String SEGMENTS_PROPERTY = "segments";
/** 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 Pattern UNESCAPED_SLASH_PATTERN = Pattern.compile("(? 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);
}
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");
}
}
private static boolean containsUnescapedSlash(String data) {
return UNESCAPED_SLASH_PATTERN.matcher(data).find();
}
/**
* 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 type root path. */
public static UniformPath testRoot() {
return new UniformPath(EType.TEST);
}
/** Escapes uniform path separators in a segment. */
public static String escapeSegment(String segment) {
return segment.replace("/", "\\/");
}
/**
* 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;
}
/**
* 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 issue path */
public boolean isIssuePath() {
return type == EType.ISSUES;
}
/** Returns whether this path is a test execution path */
public boolean isTestPath() {
return type == EType.TEST;
}
/**
* 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
public String toString() {
if (isCodePath()) {
return StringUtils.concat(segments, "/");
}
return type.getPrefix() + "/" + StringUtils.concat(segments, "/");
}
@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() {
try {
return URLEncoder.encode(toString(), FileSystemUtils.UTF8_ENCODING);
} catch (UnsupportedEncodingException e) {
// javadoc of encode says that w3c recommends to use UTF-8, so this is extremely
// unlikely
CCSMAssert.fail("URLEncoder does not support UTF-8 anymore.", e);
return null;
}
}
/** All types a path can have. */
@ExportToTypeScript
public enum EType {
/** Code path (default). */
CODE("-code-"),
/** Non-code path. */
NON_CODE("-non-code-"),
/** Architecture path. */
ARCHITECTURE("-architectures-"),
/** Test path. */
TEST("-test-"),
/** Issue path. */
ISSUES("-issues-");
/** 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