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