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.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023import java.util.ListIterator;
024
025import org.conqat.lib.commons.collections.CollectionUtils;
026import org.conqat.lib.commons.string.StringUtils;
027
028import com.google.common.base.Preconditions;
029import com.google.common.collect.Iterables;
030
031/**
032 * A relative uniform path that can be resolved against an absolute uniform
033 * path. In contrast to absolute uniform paths, relative paths are not typed.
034 */
035public final class RelativeUniformPath {
036
037        /**
038         * The constituent segments of this uniform path, e.g. for the path
039         * {@code src/main} this would be the array {@code [src, main]}. Segments may
040         * consist of dots or be empty.
041         */
042        private final String[] segments;
043
044        private RelativeUniformPath(String... pathSegments) {
045                this(Arrays.asList(pathSegments));
046        }
047
048        private RelativeUniformPath(Iterable<String> pathSegments) {
049                this.segments = Iterables.toArray(pathSegments, String.class);
050        }
051
052        /**
053         * Creates a relative uniform path from the given segments. No resolution of
054         * relative segments (such as {@code ../src}) is attempted at this stage.
055         * 
056         * @see #of(List)
057         */
058        public static RelativeUniformPath of(String... segments) {
059                Preconditions.checkNotNull(segments, UniformPath.SEGMENTS_LIST_MAY_NOT_BE_NULL);
060
061                return of(Arrays.asList(segments));
062        }
063
064        /**
065         * Creates a relative uniform path from the given segments. No resolution of
066         * relative segments (such as {@code ../src}) is attempted at this stage.
067         */
068        public static RelativeUniformPath of(List<String> segments) {
069                Preconditions.checkNotNull(segments, UniformPath.SEGMENTS_LIST_MAY_NOT_BE_NULL);
070
071                for (String segment : segments) {
072                        UniformPath.checkSegmentValidity(segment, segments, s -> s == null, "empty segment");
073                        UniformPath.checkSegmentValidity(segment, segments,
074                                        s -> StringUtils.splitWithEscapeCharacter(s, "/").size() > 1, "contains unescaped slash");
075                }
076                if (segments.isEmpty()) {
077                        return new RelativeUniformPath(new String[0]);
078                }
079                return new RelativeUniformPath(segments);
080        }
081
082        /**
083         * Resolves relative path segments such as (such as {@code src/main/../java}) to
084         * the absolute segments of the path (e.g. {@code [src, java]}).
085         */
086        /* package */ static List<String> resolveRelativeSegments(List<String> pathSegments) {
087                List<String> canonicalSegments = CollectionUtils.filter(pathSegments,
088                                segment -> !(StringUtils.isEmpty(segment) || segment.equals(".")));
089                ListIterator<String> canonicalSegmentsIterator = canonicalSegments.listIterator();
090                while (canonicalSegmentsIterator.hasNext()) {
091                        if (canonicalSegmentsIterator.next().equals("..")) {
092                                // Remove both previous element and the ".." item
093                                canonicalSegmentsIterator.previous();
094                                canonicalSegmentsIterator.remove();
095                                if (!canonicalSegmentsIterator.hasPrevious()) {
096                                        throw new IllegalArgumentException(
097                                                        "Invalid path (refers outside the root folder): " + String.join("/", pathSegments));
098                                }
099                                canonicalSegmentsIterator.previous();
100                                canonicalSegmentsIterator.remove();
101                        }
102                }
103                return canonicalSegments;
104        }
105
106        /** Returns a read-only view of the segments of this relative uniform path. */
107        /* package */ List<String> getSegments() {
108                return Collections.unmodifiableList(Arrays.asList(segments));
109        }
110
111        /** Returns the name of the last segment of this path. */
112        public String getLastSegment() {
113                if (segments.length == 0) {
114                        throw new IllegalArgumentException("Cannot get the last segment of the root path");
115                }
116                return segments[segments.length - 1];
117        }
118
119        /**
120         * Creates a new {@link RelativeUniformPath} by prepending the given suffixes to
121         * the relative uniform path.
122         */
123        public RelativeUniformPath addSuffix(String... suffix) {
124                String[] newSegments = Arrays.copyOf(this.segments, this.segments.length + suffix.length);
125                System.arraycopy(suffix, 0, newSegments, this.segments.length, suffix.length);
126                return RelativeUniformPath.of(newSegments);
127        }
128
129        /**
130         * Resolves this relative path against the given absolute path, performing path
131         * canonicalization in the process (i.e. ".." will be resolved to parent
132         * segment).
133         */
134        public UniformPath resolveAgainstAbsolutePath(UniformPath uniformPath) {
135                return uniformPath.resolve(this);
136        }
137
138        @Override
139        public String toString() {
140                return String.join("/", segments);
141        }
142
143        @Override
144        public int hashCode() {
145                return Arrays.hashCode(segments);
146        }
147
148        @Override
149        public boolean equals(Object obj) {
150                if (this == obj) {
151                        return true;
152                }
153                if (obj == null) {
154                        return false;
155                }
156                if (!(obj instanceof RelativeUniformPath)) {
157                        return false;
158                }
159                RelativeUniformPath other = (RelativeUniformPath) obj;
160                return Arrays.equals(segments, other.segments);
161        }
162}