1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.shared.utils;
20
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23
24 import java.io.File;
25 import java.util.StringTokenizer;
26
27 /**
28 * <p>Path tool contains static methods to assist in determining path-related
29 * information such as relative paths.</p>
30 * <p>
31 * This class originally got developed at Apache Anakia and later maintained
32 * in maven-utils of Apache Maven-1.
33 * Some external fixes by Apache Committers have been applied later.
34 * </p>
35 */
36 public class PathTool {
37
38 /**
39 * The constructor.
40 *
41 * @deprecated This is a utility class with only static methods. Don't create instances of it.
42 */
43 @Deprecated
44 public PathTool() {}
45
46 /**
47 * Determines the relative path of a filename from a base directory.
48 * This method is useful in building relative links within pages of
49 * a web site. It provides similar functionality to Anakia's
50 * <code>$relativePath</code> context variable. The arguments to
51 * this method may contain either forward or backward slashes as
52 * file separators. The relative path returned is formed using
53 * forward slashes as it is expected this path is to be used as a
54 * link in a web page (again mimicking Anakia's behavior).
55 * <p>
56 * This method is thread-safe.
57 * </p>
58 * <pre>
59 * PathTool.getRelativePath( null, null ) = ""
60 * PathTool.getRelativePath( null, "/usr/local/java/bin" ) = ""
61 * PathTool.getRelativePath( "/usr/local/", null ) = ""
62 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" ) = ".."
63 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.."
64 * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = ""
65 * </pre>
66 *
67 * @param basedir The base directory.
68 * @param filename The filename that is relative to the base
69 * directory.
70 * @return The relative path of the filename from the base
71 * directory. This value is not terminated with a forward slash.
72 * A zero-length string is returned if: the filename is not relative to
73 * the base directory, <code>basedir</code> is null or zero-length,
74 * or <code>filename</code> is null or zero-length.
75 */
76 public static String getRelativePath(@Nullable String basedir, @Nullable String filename) {
77 basedir = uppercaseDrive(basedir);
78 filename = uppercaseDrive(filename);
79
80 /*
81 * Verify the arguments and make sure the filename is relative
82 * to the base directory.
83 */
84 if (basedir == null
85 || basedir.length() == 0
86 || filename == null
87 || filename.length() == 0
88 || !filename.startsWith(basedir)) {
89 return "";
90 }
91
92 /*
93 * Normalize the arguments. First, determine the file separator
94 * that is being used, then strip that off the end of both the
95 * base directory and filename.
96 */
97 String separator = determineSeparator(filename);
98 basedir = StringUtils.chompLast(basedir, separator);
99 filename = StringUtils.chompLast(filename, separator);
100
101 /*
102 * Remove the base directory from the filename to end up with a
103 * relative filename (relative to the base directory). This
104 * filename is then used to determine the relative path.
105 */
106 String relativeFilename = filename.substring(basedir.length());
107
108 return determineRelativePath(relativeFilename, separator);
109 }
110
111 /**
112 * <p>This method can calculate the relative path between two paths on a file system.</p>
113 * <pre>
114 * PathTool.getRelativeFilePath( null, null ) = ""
115 * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = ""
116 * PathTool.getRelativeFilePath( "/usr/local", null ) = ""
117 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin"
118 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin"
119 * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.."
120 * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
121 * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
122 * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin"
123 * PathTool.getRelativeFilePath( "/bin", "/usr/local/" ) = "../usr/local"
124 * </pre>
125 * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
126 *
127 * @param oldPath old path
128 * @param newPath new path
129 * @return a relative file path from <code>oldPath</code>.
130 */
131 public static String getRelativeFilePath(final String oldPath, final String newPath) {
132 if (StringUtils.isEmpty(oldPath) || StringUtils.isEmpty(newPath)) {
133 return "";
134 }
135
136 // normalise the path delimiters
137 String fromPath = new File(oldPath).getPath();
138 String toPath = new File(newPath).getPath();
139
140 // strip any leading slashes if its a windows path
141 if (toPath.matches("^\\[a-zA-Z]:")) {
142 toPath = toPath.substring(1);
143 }
144 if (fromPath.matches("^\\[a-zA-Z]:")) {
145 fromPath = fromPath.substring(1);
146 }
147
148 // lowercase windows drive letters.
149 if (fromPath.startsWith(":", 1)) {
150 fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1);
151 }
152 if (toPath.startsWith(":", 1)) {
153 toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1);
154 }
155
156 // check for the presence of windows drives. No relative way of
157 // traversing from one to the other.
158 if ((toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))
159 && (!toPath.substring(0, 1).equals(fromPath.substring(0, 1)))) {
160 // they both have drive path element but they dont match, no
161 // relative path
162 return null;
163 }
164
165 if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1))
166 || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) {
167 // one has a drive path element and the other doesnt, no relative
168 // path.
169 return null;
170 }
171
172 String resultPath = buildRelativePath(toPath, fromPath, File.separatorChar);
173
174 if (newPath.endsWith(File.separator) && !resultPath.endsWith(File.separator)) {
175 return resultPath + File.separator;
176 }
177
178 return resultPath;
179 }
180
181 // ----------------------------------------------------------------------
182 // Private methods
183 // ----------------------------------------------------------------------
184
185 /**
186 * Determines the relative path of a filename. For each separator
187 * within the filename (except the leading if present), append the
188 * "../" string to the return value.
189 *
190 * @param filename The filename to parse.
191 * @param separator The separator used within the filename.
192 * @return The relative path of the filename. This value is not
193 * terminated with a forward slash. A zero-length string is
194 * returned if: the filename is zero-length.
195 */
196 @Nonnull
197 private static String determineRelativePath(@Nonnull String filename, @Nonnull String separator) {
198 if (filename.length() == 0) {
199 return "";
200 }
201
202 /*
203 * Count the slashes in the relative filename, but exclude the
204 * leading slash. If the path has no slashes, then the filename
205 * is relative to the current directory.
206 */
207 int slashCount = StringUtils.countMatches(filename, separator) - 1;
208 if (slashCount <= 0) {
209 return ".";
210 }
211
212 /*
213 * The relative filename contains one or more slashes indicating
214 * that the file is within one or more directories. Thus, each
215 * slash represents a "../" in the relative path.
216 */
217 StringBuilder sb = new StringBuilder();
218 for (int i = 0; i < slashCount; i++) {
219 sb.append("../");
220 }
221
222 /*
223 * Finally, return the relative path but strip the trailing
224 * slash to mimic Anakia's behavior.
225 */
226 return StringUtils.chop(sb.toString());
227 }
228
229 /**
230 * Helper method to determine the file separator (forward or
231 * backward slash) used in a filename. The slash that occurs more
232 * often is returned as the separator.
233 *
234 * @param filename The filename parsed to determine the file
235 * separator.
236 * @return The file separator used within <code>filename</code>.
237 * This value is either a forward or backward slash.
238 */
239 private static String determineSeparator(String filename) {
240 int forwardCount = StringUtils.countMatches(filename, "/");
241 int backwardCount = StringUtils.countMatches(filename, "\\");
242
243 return forwardCount >= backwardCount ? "/" : "\\";
244 }
245
246 /**
247 * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase
248 *
249 * @param path old path
250 * @return String
251 */
252 static String uppercaseDrive(@Nullable String path) {
253 if (path == null) {
254 return null;
255 }
256 if (path.length() >= 2 && path.charAt(1) == ':') {
257 path = Character.toUpperCase(path.charAt(0)) + path.substring(1);
258 }
259 return path;
260 }
261
262 @Nonnull
263 private static String buildRelativePath(
264 @Nonnull String toPath, @Nonnull String fromPath, final char separatorChar) {
265 // use tokeniser to traverse paths and for lazy checking
266 StringTokenizer toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
267 StringTokenizer fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
268
269 int count = 0;
270
271 // walk along the to path looking for divergence from the from path
272 while (toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens()) {
273 if (separatorChar == '\\') {
274 if (!fromTokeniser.nextToken().equalsIgnoreCase(toTokeniser.nextToken())) {
275 break;
276 }
277 } else {
278 if (!fromTokeniser.nextToken().equals(toTokeniser.nextToken())) {
279 break;
280 }
281 }
282
283 count++;
284 }
285
286 // reinitialise the tokenisers to count positions to retrieve the
287 // gobbled token
288
289 toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
290 fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
291
292 while (count-- > 0) {
293 fromTokeniser.nextToken();
294 toTokeniser.nextToken();
295 }
296
297 StringBuilder relativePath = new StringBuilder();
298
299 // add back refs for the rest of from location.
300 while (fromTokeniser.hasMoreTokens()) {
301 fromTokeniser.nextToken();
302
303 relativePath.append("..");
304
305 if (fromTokeniser.hasMoreTokens()) {
306 relativePath.append(separatorChar);
307 }
308 }
309
310 if (relativePath.length() != 0 && toTokeniser.hasMoreTokens()) {
311 relativePath.append(separatorChar);
312 }
313
314 // add fwd fills for whatevers left of newPath.
315 while (toTokeniser.hasMoreTokens()) {
316 relativePath.append(toTokeniser.nextToken());
317
318 if (toTokeniser.hasMoreTokens()) {
319 relativePath.append(separatorChar);
320 }
321 }
322 return relativePath.toString();
323 }
324 }