1 package org.codehaus.plexus.util;
2
3 /*
4 * Copyright The Codehaus Foundation.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * 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, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import java.io.File;
20 import java.util.StringTokenizer;
21
22 /**
23 * Path tool contains static methods to assist in determining path-related information such as relative paths.
24 *
25 * @author <a href="mailto:pete-apache-dev@kazmier.com">Pete Kazmier</a>
26 * @author <a href="mailto:vmassol@apache.org">Vincent Massol</a>
27 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
28 *
29 */
30 public class PathTool
31 {
32 /**
33 * <p>Determines the relative path of a filename from a base directory. This method is useful in building relative
34 * links within pages of a web site. It provides similar functionality to Anakia's <code>$relativePath</code>
35 * context variable. The arguments to this method may contain either forward or backward slashes as file separators.
36 * The relative path returned is formed using forward slashes as it is expected this path is to be used as a link in
37 * a web page (again mimicking Anakia's behavior).</p>
38 *
39 * <p>This method is thread-safe.</p>
40 *
41 * <pre>
42 * PathTool.getRelativePath( null, null ) = ""
43 * PathTool.getRelativePath( null, "/usr/local/java/bin" ) = ""
44 * PathTool.getRelativePath( "/usr/local/", null ) = ""
45 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" ) = ".."
46 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.."
47 * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = ""
48 * </pre>
49 *
50 * @param basedir The base directory.
51 * @param filename The filename that is relative to the base directory.
52 * @return The relative path of the filename from the base directory. This value is not terminated with a forward
53 * slash. A zero-length string is returned if: the filename is not relative to the base directory,
54 * <code>basedir</code> is null or zero-length, or <code>filename</code> is null or zero-length.
55 */
56 public static final String getRelativePath( String basedir, String filename )
57 {
58 basedir = uppercaseDrive( basedir );
59 filename = uppercaseDrive( filename );
60
61 /*
62 * Verify the arguments and make sure the filename is relative to the base directory.
63 */
64 if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0
65 || !filename.startsWith( basedir ) )
66 {
67 return "";
68 }
69
70 /*
71 * Normalize the arguments. First, determine the file separator that is being used, then strip that off the end
72 * of both the base directory and filename.
73 */
74 String separator = determineSeparator( filename );
75 basedir = StringUtils.chompLast( basedir, separator );
76 filename = StringUtils.chompLast( filename, separator );
77
78 /*
79 * Remove the base directory from the filename to end up with a relative filename (relative to the base
80 * directory). This filename is then used to determine the relative path.
81 */
82 String relativeFilename = filename.substring( basedir.length() );
83
84 return determineRelativePath( relativeFilename, separator );
85 }
86
87 /**
88 * <p>Determines the relative path of a filename. This method is useful in building relative links within pages of a
89 * web site. It provides similar functionality to Anakia's <code>$relativePath</code> context variable. The argument
90 * to this method may contain either forward or backward slashes as file separators. The relative path returned is
91 * formed using forward slashes as it is expected this path is to be used as a link in a web page (again mimicking
92 * Anakia's behavior).</p>
93 *
94 * <p>This method is thread-safe.</p>
95 *
96 * @param filename The filename to be parsed.
97 * @return The relative path of the filename. This value is not terminated with a forward slash. A zero-length
98 * string is returned if: <code>filename</code> is null or zero-length.
99 * @see #getRelativeFilePath(String, String)
100 */
101 public static final String getRelativePath( String filename )
102 {
103 filename = uppercaseDrive( filename );
104
105 if ( filename == null || filename.length() == 0 )
106 {
107 return "";
108 }
109
110 /*
111 * Normalize the argument. First, determine the file separator that is being used, then strip that off the end
112 * of the filename. Then, if the filename doesn't begin with a separator, add one.
113 */
114
115 String separator = determineSeparator( filename );
116 filename = StringUtils.chompLast( filename, separator );
117 if ( !filename.startsWith( separator ) )
118 {
119 filename = separator + filename;
120 }
121
122 return determineRelativePath( filename, separator );
123 }
124
125 /**
126 * <p>Determines the directory component of a filename. This is useful within DVSL templates when used in conjunction
127 * with the DVSL's <code>$context.getAppValue("infilename")</code> to get the current directory that is currently
128 * being processed.</p>
129 *
130 * <p>This method is thread-safe.</p>
131 *
132 * <pre>
133 * PathTool.getDirectoryComponent( null ) = ""
134 * PathTool.getDirectoryComponent( "/usr/local/java/bin" ) = "/usr/local/java"
135 * PathTool.getDirectoryComponent( "/usr/local/java/bin/" ) = "/usr/local/java/bin"
136 * PathTool.getDirectoryComponent( "/usr/local/java/bin/java.sh" ) = "/usr/local/java/bin"
137 * </pre>
138 *
139 * @param filename The filename to be parsed.
140 * @return The directory portion of the <code>filename</code>. If the filename does not contain a directory
141 * component, "." is returned.
142 */
143 public static final String getDirectoryComponent( String filename )
144 {
145 if ( filename == null || filename.length() == 0 )
146 {
147 return "";
148 }
149
150 String separator = determineSeparator( filename );
151 String directory = StringUtils.chomp( filename, separator );
152
153 if ( filename.equals( directory ) )
154 {
155 return ".";
156 }
157
158 return directory;
159 }
160
161 /**
162 * Calculates the appropriate link given the preferred link and the relativePath of the document.
163 *
164 * <pre>
165 * PathTool.calculateLink( "/index.html", "../.." ) = "../../index.html"
166 * PathTool.calculateLink( "http://plexus.codehaus.org/plexus-utils/index.html", "../.." ) = "http://plexus.codehaus.org/plexus-utils/index.html"
167 * PathTool.calculateLink( "/usr/local/java/bin/java.sh", "../.." ) = "../../usr/local/java/bin/java.sh"
168 * PathTool.calculateLink( "../index.html", "/usr/local/java/bin" ) = "/usr/local/java/bin/../index.html"
169 * PathTool.calculateLink( "../index.html", "http://plexus.codehaus.org/plexus-utils" ) = "http://plexus.codehaus.org/plexus-utils/../index.html"
170 * </pre>
171 *
172 * @param link main link
173 * @param relativePath relative
174 * @return String
175 */
176 public static final String calculateLink( String link, String relativePath )
177 {
178 if ( link == null )
179 {
180 link = "";
181 }
182 if ( relativePath == null )
183 {
184 relativePath = "";
185 }
186 // This must be some historical feature
187 if ( link.startsWith( "/site/" ) )
188 {
189 return link.substring( 5 );
190 }
191
192 // Allows absolute links in nav-bars etc
193 if ( link.startsWith( "/absolute/" ) )
194 {
195 return link.substring( 10 );
196 }
197
198 // This traps urls like http://
199 if ( link.contains( ":" ) )
200 {
201 return link;
202 }
203
204 // If relativepath is current directory, just pass the link through
205 if ( StringUtils.equals( relativePath, "." ) )
206 {
207 if ( link.startsWith( "/" ) )
208 {
209 return link.substring( 1 );
210 }
211
212 return link;
213 }
214
215 // If we don't do this, you can end up with ..//bob.html rather than ../bob.html
216 if ( relativePath.endsWith( "/" ) && link.startsWith( "/" ) )
217 {
218 return relativePath + "." + link.substring( 1 );
219 }
220
221 if ( relativePath.endsWith( "/" ) || link.startsWith( "/" ) )
222 {
223 return relativePath + link;
224 }
225
226 return relativePath + "/" + link;
227 }
228
229 /**
230 * This method can calculate the relative path between two paths on a web site.
231 *
232 * <pre>
233 * PathTool.getRelativeWebPath( null, null ) = ""
234 * PathTool.getRelativeWebPath( null, "http://plexus.codehaus.org/" ) = ""
235 * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/", null ) = ""
236 * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/",
237 * "http://plexus.codehaus.org/plexus-utils/index.html" ) = "plexus-utils/index.html"
238 * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/plexus-utils/index.html",
239 * "http://plexus.codehaus.org/" = "../../"
240 * </pre>
241 *
242 * @param oldPath main path
243 * @param newPath second path
244 * @return a relative web path from <code>oldPath</code>.
245 */
246 public static final String getRelativeWebPath( final String oldPath, final String newPath )
247 {
248 if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) )
249 {
250 return "";
251 }
252
253 String resultPath = buildRelativePath( newPath, oldPath, '/' );
254
255 if ( newPath.endsWith( "/" ) && !resultPath.endsWith( "/" ) )
256 {
257 return resultPath + "/";
258 }
259
260 return resultPath;
261 }
262
263 /**
264 * This method can calculate the relative path between two paths on a file system.
265 *
266 * <pre>
267 * PathTool.getRelativeFilePath( null, null ) = ""
268 * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = ""
269 * PathTool.getRelativeFilePath( "/usr/local", null ) = ""
270 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin"
271 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin"
272 * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.."
273 * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
274 * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
275 * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin"
276 * PathTool.getRelativeFilePath( "/bin", "/usr/local/" ) = "../usr/local"
277 * </pre>
278 *
279 * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
280 *
281 * @param oldPath main path
282 * @param newPath second path
283 * @return a relative file path from <code>oldPath</code>.
284 */
285 public static final String getRelativeFilePath( final String oldPath, final String newPath )
286 {
287 if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) )
288 {
289 return "";
290 }
291
292 // normalise the path delimiters
293 String fromPath = new File( oldPath ).getPath();
294 String toPath = new File( newPath ).getPath();
295
296 // strip any leading slashes if its a windows path
297 if ( toPath.matches( "^\\[a-zA-Z]:" ) )
298 {
299 toPath = toPath.substring( 1 );
300 }
301 if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
302 {
303 fromPath = fromPath.substring( 1 );
304 }
305
306 // lowercase windows drive letters.
307 if ( fromPath.startsWith( ":", 1 ) )
308 {
309 fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
310 }
311 if ( toPath.startsWith( ":", 1 ) )
312 {
313 toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
314 }
315
316 // check for the presence of windows drives. No relative way of
317 // traversing from one to the other.
318 if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
319 && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
320 {
321 // they both have drive path element but they dont match, no
322 // relative path
323 return null;
324 }
325
326 if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
327 || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
328 {
329 // one has a drive path element and the other doesnt, no relative
330 // path.
331 return null;
332 }
333
334 String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar );
335
336 if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) )
337 {
338 return resultPath + File.separator;
339 }
340
341 return resultPath;
342 }
343
344 // ----------------------------------------------------------------------
345 // Private methods
346 // ----------------------------------------------------------------------
347
348 /**
349 * Determines the relative path of a filename. For each separator within the filename (except the leading if
350 * present), append the "../" string to the return value.
351 *
352 * @param filename The filename to parse.
353 * @param separator The separator used within the filename.
354 * @return The relative path of the filename. This value is not terminated with a forward slash. A zero-length
355 * string is returned if: the filename is zero-length.
356 */
357 private static final String determineRelativePath( String filename, String separator )
358 {
359 if ( filename.length() == 0 )
360 {
361 return "";
362 }
363
364 /*
365 * Count the slashes in the relative filename, but exclude the leading slash. If the path has no slashes, then
366 * the filename is relative to the current directory.
367 */
368 int slashCount = StringUtils.countMatches( filename, separator ) - 1;
369 if ( slashCount <= 0 )
370 {
371 return ".";
372 }
373
374 /*
375 * The relative filename contains one or more slashes indicating that the file is within one or more
376 * directories. Thus, each slash represents a "../" in the relative path.
377 */
378 StringBuilder sb = new StringBuilder();
379 for ( int i = 0; i < slashCount; i++ )
380 {
381 sb.append( "../" );
382 }
383
384 /*
385 * Finally, return the relative path but strip the trailing slash to mimic Anakia's behavior.
386 */
387 return StringUtils.chop( sb.toString() );
388 }
389
390 /**
391 * Helper method to determine the file separator (forward or backward slash) used in a filename. The slash that
392 * occurs more often is returned as the separator.
393 *
394 * @param filename The filename parsed to determine the file separator.
395 * @return The file separator used within <code>filename</code>. This value is either a forward or backward slash.
396 */
397 private static final String determineSeparator( String filename )
398 {
399 int forwardCount = StringUtils.countMatches( filename, "/" );
400 int backwardCount = StringUtils.countMatches( filename, "\\" );
401
402 return forwardCount >= backwardCount ? "/" : "\\";
403 }
404
405 /**
406 * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase
407 *
408 * @param path
409 * @return String
410 */
411 static final String uppercaseDrive( String path )
412 {
413 if ( path == null )
414 {
415 return null;
416 }
417 if ( path.length() >= 2 && path.charAt( 1 ) == ':' )
418 {
419 path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 );
420 }
421 return path;
422 }
423
424 private static final String buildRelativePath( String toPath, String fromPath, final char separatorChar )
425 {
426 // use tokeniser to traverse paths and for lazy checking
427 StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
428 StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
429
430 int count = 0;
431
432 // walk along the to path looking for divergence from the from path
433 while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
434 {
435 if ( separatorChar == '\\' )
436 {
437 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
438 {
439 break;
440 }
441 }
442 else
443 {
444 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
445 {
446 break;
447 }
448 }
449
450 count++;
451 }
452
453 // reinitialise the tokenisers to count positions to retrieve the
454 // gobbled token
455
456 toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
457 fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
458
459 while ( count-- > 0 )
460 {
461 fromTokeniser.nextToken();
462 toTokeniser.nextToken();
463 }
464
465 String relativePath = "";
466
467 // add back refs for the rest of from location.
468 while ( fromTokeniser.hasMoreTokens() )
469 {
470 fromTokeniser.nextToken();
471
472 relativePath += "..";
473
474 if ( fromTokeniser.hasMoreTokens() )
475 {
476 relativePath += separatorChar;
477 }
478 }
479
480 if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
481 {
482 relativePath += separatorChar;
483 }
484
485 // add fwd fills for whatevers left of newPath.
486 while ( toTokeniser.hasMoreTokens() )
487 {
488 relativePath += toTokeniser.nextToken();
489
490 if ( toTokeniser.hasMoreTokens() )
491 {
492 relativePath += separatorChar;
493 }
494 }
495 return relativePath;
496 }
497 }