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 }