001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 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+-------------------------------------------------------------------------*/
017package org.conqat.engine.resource.util;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.conqat.lib.commons.string.StringUtils;
026
027/**
028 * Utility methods for dealing with uniform paths.
029 */
030public class UniformPathUtils {
031
032        /**
033         * Matches windows drive letters prefixes, e.g. "C:/", and captures the path
034         * after the drive letter as first group. Requires the path to be normalized
035         * before matching, as the pattern will not match '\'.
036         */
037        private static final Pattern DRIVE_LETTER_PATTERN = Pattern.compile("[A-Za-z]:/(.*)");
038
039        /** The character used as path separator in uniform paths. */
040        public static final char SEPARATOR_CHAR = '/';
041
042        /** String representation of {@link #SEPARATOR_CHAR}. */
043        public static final String SEPARATOR = String.valueOf(SEPARATOR_CHAR);
044
045        /** Pattern used for splitting along the separator. */
046        private static final Pattern SPLIT_PATTERN = Pattern.compile("(?<!\\\\)" + SEPARATOR);
047
048        /**
049         * Extracts the project part of a uniform path, which is everything up to the
050         * first {@link #SEPARATOR_CHAR}.
051         */
052        public static String extractProject(String uniformPath) {
053                return StringUtils.getFirstParts(uniformPath, 1, SEPARATOR_CHAR);
054        }
055
056        /**
057         * Returns the path without the project, i.e. removes everything up to the first
058         * {@link #SEPARATOR_CHAR}.
059         */
060        public static String stripProject(String uniformPath) {
061                int pos = uniformPath.indexOf(SEPARATOR_CHAR);
062                if (pos >= 0) {
063                        return uniformPath.substring(pos + 1);
064                }
065                return uniformPath;
066        }
067
068        /**
069         * Returns the element name for a uniform path, which is the everything starting
070         * from the last {@link #SEPARATOR_CHAR}.
071         */
072        public static String getElementName(String uniformPath) {
073                return StringUtils.getLastPart(uniformPath, SEPARATOR_CHAR);
074        }
075
076        /**
077         * Returns the parent path for a path which is everything up to the last
078         * non-escaped {@link #SEPARATOR_CHAR}. If no separator is found, the empty
079         * string is returned.
080         */
081        public static String getParentPath(String uniformPath) {
082                // we can not use StringUtils.removeLastPart(), as the behavior for a
083                // string without separator is different here
084                Matcher matcher = SPLIT_PATTERN.matcher(uniformPath);
085                int idx = -1;
086                while (matcher.find()) {
087                        idx = matcher.start();
088                }
089                if (idx == -1) {
090                        return StringUtils.EMPTY_STRING;
091                }
092                return uniformPath.substring(0, idx);
093        }
094
095        /** Removes the first <code>count</code> segments from the given path. */
096        public static String removeFirstSegments(String uniformPath, int count) {
097                String[] segments = splitPath(uniformPath);
098                return concatenate(Arrays.copyOfRange(segments, count, segments.length));
099        }
100
101        /** Removes the last <code>count</code> segments from the given path. */
102        public static String removeLastSegments(String uniformPath, int count) {
103                String[] segments = splitPath(uniformPath);
104                return concatenate(Arrays.copyOfRange(segments, 0, segments.length - count));
105        }
106
107        /** Returns segments forming the given path. */
108        public static String[] splitPath(String uniformPath) {
109                return SPLIT_PATTERN.split(uniformPath);
110        }
111
112        /**
113         * Returns the extension of the uniform path.
114         * 
115         * @return File extension, i.e. "java" for "FileSystemUtils.java", or
116         *         <code>null</code>, if the path has no extension (i.e. if a path
117         *         contains no '.'), returns the empty string if the '.' is the path's
118         *         last character.
119         */
120        public static String getExtension(String uniformPath) {
121                String name = getElementName(uniformPath);
122                int posLastDot = name.lastIndexOf('.');
123                if (posLastDot < 0) {
124                        return null;
125                }
126                return name.substring(posLastDot + 1);
127        }
128
129        /**
130         * Replaces forward and backward slashes, not only system-specific separators,
131         * with a forward slash. We do this on purpose, since paths that e.g. are read
132         * from files do not necessarily contain the separators contained in
133         * File.separator.
134         */
135        public static String normalizeAllSeparators(String path) {
136                return path.replaceAll("[/\\\\]+", "/");
137        }
138
139        /**
140         * Creates a clean path by resolving duplicate slashes, single and double dots.
141         * This is the equivalent to path canonization on uniform paths.
142         */
143        public static String cleanPath(String path) {
144                String[] parts = splitPath(path);
145                for (int i = 0; i < parts.length; ++i) {
146                        // do not use StringUtils.isEmpty(), as we do not want trim
147                        // semantics!
148                        if (StringUtils.EMPTY_STRING.equals(parts[i]) || ".".equals(parts[i])) {
149                                parts[i] = null;
150                        } else if ("..".equals(parts[i])) {
151                                // cancel last non-null (if any)
152                                int j = i - 1;
153                                for (; j >= 0; --j) {
154                                        if ("..".equals(parts[j])) {
155                                                // another '..' acts as boundary
156                                                break;
157                                        }
158                                        if (parts[j] != null) {
159                                                // cancel both parts of the path.
160                                                parts[j] = null;
161                                                parts[i] = null;
162                                                break;
163                                        }
164                                }
165                        }
166                }
167
168                return joinPath(parts);
169        }
170
171        /** Joins the given array as a path, but ignoring null entries. */
172        private static String joinPath(String[] parts) {
173                StringBuilder sb = new StringBuilder();
174                for (String part : parts) {
175                        if (part != null) {
176                                if (sb.length() > 0) {
177                                        sb.append(SEPARATOR_CHAR);
178                                }
179                                sb.append(part);
180                        }
181                }
182
183                return sb.toString();
184        }
185
186        /**
187         * For a uniform path denoting a file and a relative path, constructs the
188         * uniform path for the relatively addressed element.
189         */
190        public static String resolveRelativePath(String basePath, String relative) {
191                // obtain "directory" from path denoting file
192                String directory = getParentPath(basePath);
193                if (!directory.isEmpty()) {
194                        directory += SEPARATOR;
195                }
196                return cleanPath(directory + relative);
197        }
198
199        /**
200         * Returns the concatenated path of all given parts. Empty or null strings are
201         * ignored.
202         */
203        public static String concatenate(String... parts) {
204                List<String> list = new ArrayList<String>(parts.length);
205                for (String part : parts) {
206                        if (!StringUtils.isEmpty(part)) {
207                                list.add(part);
208                        }
209                }
210                return StringUtils.concat(list, SEPARATOR);
211        }
212
213        /**
214         * Creates a uniform path by prepending the project to the given path. If the
215         * project is null, it is ignored.
216         */
217        public static String prependProject(String projectName, String path) {
218                return cleanPath(concatenate(projectName, path));
219        }
220
221        /**
222         * Remove drive letters or unix root slash from path. Also normalizes the path.
223         */
224        public static String createSystemIndependentPath(String path) {
225                Matcher m1 = DRIVE_LETTER_PATTERN.matcher(normalizeAllSeparators(path));
226                if (m1.matches()) {
227                        // Remove drive letter
228                        path = m1.group(1);
229                }
230
231                // Remove unix root slash
232                path = StringUtils.stripPrefix(path, "/");
233                return path;
234        }
235
236}