All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.lib.commons.uniformpath.UniformPath Maven / Gradle / Ivy

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