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