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}