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

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

/*-------------------------------------------------------------------------+
|                                                                          |
| 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