001/*-------------------------------------------------------------------------+ 002| | 003| Copyright (c) 2005-2018 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016| | 017+-------------------------------------------------------------------------*/ 018package org.conqat.lib.commons.uniformpath; 019 020import java.io.Serializable; 021import java.io.UnsupportedEncodingException; 022import java.net.URLEncoder; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Optional; 028import java.util.Set; 029import java.util.function.Predicate; 030import java.util.regex.Pattern; 031 032import org.conqat.lib.commons.assertion.CCSMAssert; 033import org.conqat.lib.commons.collections.CollectionUtils; 034import org.conqat.lib.commons.filesystem.FileSystemUtils; 035import org.conqat.lib.commons.js_export.ExportToJavaScript; 036import org.conqat.lib.commons.string.StringUtils; 037 038import com.fasterxml.jackson.annotation.JsonCreator; 039import com.fasterxml.jackson.annotation.JsonProperty; 040import com.google.common.base.Preconditions; 041 042/** 043 * An absolute uniform path with a specific type, where type might be something 044 * like "code", "non-code" or "architecture" (see {@link UniformPath.EType}). 045 * Never includes a project or repository name. 046 * 047 * Use {@link UniformPathCompatibilityUtil} to create {@link UniformPath} 048 * instances from {@link String} representations. 049 */ 050public final class UniformPath implements Comparable<UniformPath>, Serializable { 051 052 private static final long serialVersionUID = 1L; 053 054 /** The name of the JSON property name for {@link #type}. */ 055 protected static final String TYPE_PROPERTY = "type"; 056 057 /** The name of the JSON property name for {@link #segments}. */ 058 protected static final String SEGMENTS_PROPERTY = "segments"; 059 060 /** Error message thrown for null parameter. */ 061 /* package */ static final String SEGMENTS_LIST_MAY_NOT_BE_NULL = "Segments list may not be null"; 062 063 /** The segments indicating an invalid relative path. */ 064 private static final Set<String> RELATIVE_SEGMENTS = new HashSet<>(Arrays.asList(".", "..")); 065 066 private static final Pattern UNESCAPED_SLASH_PATTERN = Pattern.compile("(?<!\\\\)/"); 067 068 /** The type of this uniform path. */ 069 @JsonProperty(TYPE_PROPERTY) 070 /* package */ final EType type; 071 072 /** 073 * The constituent segments of this uniform path, e.g. for the path 074 * {@code src/main} this would be the array {@code [src, main]}. A segment will 075 * never be a relative path (e.g. "." or "..") or contain unescaped forward 076 * slashes. 077 */ 078 @JsonProperty(SEGMENTS_PROPERTY) 079 private final String[] segments; 080 081 @JsonCreator 082 private UniformPath(@JsonProperty(TYPE_PROPERTY) EType pathType, 083 @JsonProperty(SEGMENTS_PROPERTY) String... pathSegments) { 084 this.type = pathType; 085 this.segments = Arrays.copyOf(pathSegments, pathSegments.length); 086 } 087 088 private UniformPath(EType pathType, List<String> pathSegments) { 089 this.type = pathType; 090 this.segments = pathSegments.toArray(new String[0]); 091 } 092 093 /** 094 * Builds a path from the given segments. The segments must consist of non-empty 095 * strings that are not just relative paths (e.g. "." or "..") and may not 096 * contain unescaped slashes ("/"). 097 * 098 * @see #of(List) 099 */ 100 public static UniformPath of(String... segments) { 101 Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL); 102 103 return of(Arrays.asList(segments)); 104 } 105 106 /** 107 * Builds a path from the given segments. The segments must consist of non-empty 108 * strings that are not just relative paths (e.g. "." or "..") and may not 109 * contain unescaped slashes ("/"). 110 * 111 * @see #of(List) 112 */ 113 public static UniformPath of(EType type, String... segments) { 114 Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL); 115 116 return of(type, Arrays.asList(segments)); 117 } 118 119 /** 120 * Builds a path from the given segments. The segments must consist of non-empty 121 * strings that are not just relative paths (e.g. "." or "..") and may not 122 * contain unescaped slashes ("/"). 123 * 124 * The result is ensured to be of the given type so the according prefix may be 125 * present, but does not have to. If the uniform path is of a different type an 126 * assertion error is thrown. 127 * 128 * @see #of(List) 129 */ 130 public static UniformPath of(EType type, List<String> segments) { 131 checkSegmentsValidity(segments); 132 checkDoesNotStartWithProjectOrRepositoryName(segments); 133 if (segments.isEmpty()) { 134 return new UniformPath(type); 135 } 136 Optional<EType> pathType = EType.parse(segments.get(0)); 137 if (pathType.isPresent()) { 138 if (pathType.get() != type) { 139 throw new IllegalArgumentException( 140 "Uniform path of type " + type + " did start with " + segments.get(0)); 141 } 142 return new UniformPath(pathType.get(), segments.subList(1, segments.size())); 143 } 144 return new UniformPath(type, segments); 145 } 146 147 /** 148 * Builds a path from the given segments. The segments must consist of non-empty 149 * strings that are not just relative paths (e.g. "." or "..") and may not 150 * contain unescaped slashes ("/"). 151 */ 152 public static UniformPath of(List<String> segments) { 153 checkSegmentsValidity(segments); 154 checkDoesNotStartWithProjectOrRepositoryName(segments); 155 if (segments.isEmpty()) { 156 return new UniformPath(EType.CODE); 157 } 158 Optional<EType> pathType = EType.parse(segments.get(0)); 159 if (pathType.isPresent()) { 160 return new UniformPath(pathType.get(), segments.subList(1, segments.size())); 161 } 162 return new UniformPath(EType.CODE, segments); 163 } 164 165 private static void checkSegmentsValidity(List<String> segments) { 166 Preconditions.checkNotNull(segments, SEGMENTS_LIST_MAY_NOT_BE_NULL); 167 168 for (String segment : segments) { 169 checkSegmentValidity(segment, segments, StringUtils::isEmpty, "empty segment"); 170 checkSegmentValidity(segment, segments, RELATIVE_SEGMENTS::contains, "relative segment"); 171 checkSegmentValidity(segment, segments, UniformPath::containsUnescapedSlash, "contains unescaped slash"); 172 } 173 } 174 175 private static boolean containsUnescapedSlash(String data) { 176 return UNESCAPED_SLASH_PATTERN.matcher(data).find(); 177 } 178 179 /** 180 * Throws an {@link IllegalArgumentException} if the given segments start with a 181 * project or repository name. 182 */ 183 private static void checkDoesNotStartWithProjectOrRepositoryName(List<String> segments) throws AssertionError { 184 if (segments.size() > 1 && EType.parse(segments.get(1)).isPresent()) { 185 String pathWithErrorHighlighting = "[" + segments.get(0) + "]/" 186 + StringUtils.concat(segments.subList(1, segments.size()), "/"); 187 throw new IllegalArgumentException( 188 "Invalid path (includes project or repository information): " + pathWithErrorHighlighting); 189 } 190 } 191 192 /** 193 * Throws an {@link IllegalArgumentException} containing the given error message 194 * if the given check detects that the given segment is invalid. 195 */ 196 /* package */ static void checkSegmentValidity(String segment, List<String> segments, Predicate<String> errorCheck, 197 String errorDetailMessage) { 198 if (errorCheck.test(segment)) { 199 List<String> pathSegmentsWithErrorHighlighting = CollectionUtils.map(segments, pathSegment -> { 200 if (errorCheck.test(pathSegment)) { 201 return "[" + pathSegment + "]"; 202 } 203 return pathSegment; 204 }); 205 throw new IllegalArgumentException(String.format("Invalid path (%s): %s", errorDetailMessage, 206 String.join("/", pathSegmentsWithErrorHighlighting))); 207 } 208 } 209 210 /** Returns the code type root path. */ 211 public static UniformPath codeRoot() { 212 return new UniformPath(EType.CODE); 213 } 214 215 /** Returns the test type root path. */ 216 public static UniformPath testRoot() { 217 return new UniformPath(EType.TEST); 218 } 219 220 /** Escapes uniform path separators in a segment. */ 221 public static String escapeSegment(String segment) { 222 return segment.replace("/", "\\/"); 223 } 224 225 /** 226 * Returns the parent of this path, i.e. a new uniform path without the last 227 * segment of this path. 228 */ 229 public UniformPath getParent() { 230 Preconditions.checkState(segments.length != 0, "Cannot get the parent of the root path"); 231 232 return new UniformPath(type, Arrays.asList(segments).subList(0, segments.length - 1)); 233 } 234 235 /** Returns the name of the last segment (directory or file) of this path. */ 236 public String getLastSegment() { 237 Preconditions.checkState(segments.length != 0, "Cannot get the last segment of the root path"); 238 239 return segments[segments.length - 1]; 240 } 241 242 /** 243 * Returns the sub path relative to the given number of top level path segments. 244 * In other words: Remove the given number of top level elements from the path. 245 * <p> 246 * Example: Removing two segments from {@code src/main/java/Class.java} will 247 * yield the relative path {@code java/Class.java}. 248 * </p> 249 */ 250 public RelativeUniformPath getSubPath(int numberOfTopLevelSegmentsToRemove) { 251 Preconditions.checkArgument(numberOfTopLevelSegmentsToRemove <= segments.length, 252 "Cannot remove more segments than are contained in this path: %s segments to remove, path is %s", 253 numberOfTopLevelSegmentsToRemove, this); 254 255 return RelativeUniformPath 256 .of(Arrays.asList(segments).subList(numberOfTopLevelSegmentsToRemove, segments.length)); 257 } 258 259 /** 260 * Returns the relative sub path after the given segment. 261 * <p> 262 * Example: The subpath after {@code java} in {@code src/main/java/Class.java} 263 * will yield the relative path {@code Class.java}. 264 * </p> 265 */ 266 public RelativeUniformPath getSubPathAfter(String segment) { 267 return RelativeUniformPath 268 .of(Arrays.asList(segments).subList(Arrays.asList(segments).indexOf(segment) + 1, segments.length)); 269 } 270 271 /** Checks whether the given uniform path is valid or not. */ 272 public static boolean isValidPath(String uniformPath) { 273 if (uniformPath == null) { 274 return false; 275 } 276 List<String> segments = UniformPathCompatibilityUtil.getAbsoluteSegments(uniformPath); 277 278 try { 279 checkSegmentsValidity(segments); 280 checkDoesNotStartWithProjectOrRepositoryName(segments); 281 } catch (IllegalArgumentException e) { 282 return false; 283 } 284 285 return true; 286 } 287 288 /** 289 * Returns whether this is a root path. Note that there can be multiple root 290 * paths, depending on the type (i.e. "-architecture-" and "-non-code-" are both 291 * root paths. The path "/" is expanded to "-code-" and is also a root path). 292 */ 293 public boolean isRoot() { 294 return segments.length == 0; 295 } 296 297 /** Returns whether this path is a (regular) code path */ 298 public boolean isCodePath() { 299 return type == EType.CODE; 300 } 301 302 /** Returns whether this path is a non-code path */ 303 public boolean isNonCodePath() { 304 return type == EType.NON_CODE; 305 } 306 307 /** Returns whether this path is an architecture path */ 308 public boolean isArchitecturePath() { 309 return type == EType.ARCHITECTURE; 310 } 311 312 /** Returns whether this path is an issue path */ 313 public boolean isIssuePath() { 314 return type == EType.ISSUES; 315 } 316 317 /** Returns whether this path is a test execution path */ 318 public boolean isTestPath() { 319 return type == EType.TEST; 320 } 321 322 /** 323 * Resolves the given relative path against this absolute path, performing path 324 * canonicalization in the process (i.e. ".." will be resolved to parent 325 * segment). 326 */ 327 public UniformPath resolve(RelativeUniformPath relativePath) { 328 List<String> segments = new ArrayList<>(Arrays.asList(this.segments)); 329 segments.addAll(relativePath.getSegments()); 330 331 return UniformPath.of(type, RelativeUniformPath.resolveRelativeSegments(segments)); 332 } 333 334 /** 335 * Returns whether the ancestorPath contains this path as a subpath. Example: 336 * {@code src} is an ancestor of {@code src/main}. 337 */ 338 public boolean hasAncestor(UniformPath ancestorPath) { 339 if (type != ancestorPath.type) { 340 return false; 341 } 342 String[] containingPathSegments = ancestorPath.segments; 343 if (containingPathSegments.length > segments.length) { 344 return false; 345 } 346 return Arrays.asList(segments).subList(0, containingPathSegments.length) 347 .equals(Arrays.asList(containingPathSegments)); 348 } 349 350 /** 351 * Returns whether this path contains the descendantPath as a subpath. Example: 352 * {@code /src/main} is a descendant of {@code /src}. 353 */ 354 public boolean hasDescendant(UniformPath descendantPath) { 355 if (type != descendantPath.type) { 356 return false; 357 } 358 String[] descendantPathSegments = descendantPath.segments; 359 if (descendantPathSegments.length < segments.length) { 360 return false; 361 } 362 return Arrays.asList(segments).equals(Arrays.asList(descendantPathSegments).subList(0, segments.length)); 363 } 364 365 @Override 366 public String toString() { 367 if (isCodePath()) { 368 return StringUtils.concat(segments, "/"); 369 } 370 return type.getPrefix() + "/" + StringUtils.concat(segments, "/"); 371 } 372 373 @Override 374 public int hashCode() { 375 int prime = 31; 376 return prime * (prime + type.hashCode()) + Arrays.hashCode(segments); 377 } 378 379 @Override 380 public boolean equals(Object obj) { 381 if (this == obj) { 382 return true; 383 } 384 if (obj == null) { 385 return false; 386 } 387 if (!(obj instanceof UniformPath)) { 388 return false; 389 } 390 UniformPath other = (UniformPath) obj; 391 return type == other.type && Arrays.equals(segments, other.segments); 392 } 393 394 @Override 395 public int compareTo(UniformPath other) { 396 int compareToResult = type.getPrefix().compareTo(other.type.getPrefix()); 397 if (compareToResult != 0) { 398 if (isCodePath() || other.isCodePath()) { 399 return toString().compareTo(other.toString()); 400 } 401 return compareToResult; 402 } 403 for (int i = 0; i < Math.min(segments.length, other.segments.length); i++) { 404 compareToResult = segments[i].compareTo(other.segments[i]); 405 if (compareToResult != 0) { 406 return compareToResult; 407 } 408 } 409 return segments.length - other.segments.length; 410 } 411 412 /** 413 * Returns this uniform path in url encoding (as needed for safely using it in 414 * teamscale URLs). For example, '/' is replaced by "%2F", '@' is replaced by 415 * "%40", and so on. 416 */ 417 public String urlEncode() { 418 try { 419 return URLEncoder.encode(toString(), FileSystemUtils.UTF8_ENCODING); 420 } catch (UnsupportedEncodingException e) { 421 // javadoc of encode says that w3c recommends to use UTF-8, so this is extremely 422 // unlikely 423 CCSMAssert.fail("URLEncoder does not support UTF-8 anymore.", e); 424 return null; 425 } 426 } 427 428 /** All types a path can have. */ 429 @ExportToJavaScript 430 public enum EType { 431 432 /** Code path (default). */ 433 CODE("-code-"), 434 435 /** Non-code path. */ 436 NON_CODE("-non-code-"), 437 438 /** Architecture path. */ 439 ARCHITECTURE("-architectures-"), 440 441 /** Test path. */ 442 TEST("-test-"), 443 444 /** Issue path. */ 445 ISSUES("-issues-"); 446 447 /** The prefix used for this path. */ 448 private final String prefix; 449 450 EType(String prefix) { 451 this.prefix = prefix; 452 } 453 454 /** Tries to parse the given type string to a path type. */ 455 public static Optional<EType> parse(String typeString) { 456 for (EType type : EType.values()) { 457 if (typeString.equals(type.getPrefix())) { 458 return Optional.of(type); 459 } 460 } 461 return Optional.empty(); 462 } 463 464 /** @see #prefix */ 465 public String getPrefix() { 466 return prefix; 467 } 468 } 469 470}