View Javadoc
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 }