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