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