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