Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
PathTool |
|
| 0.0;0 |
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 | 0 | 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 | 6 | basedir = uppercaseDrive( basedir ); |
71 | 6 | filename = uppercaseDrive( filename ); |
72 | ||
73 | /* | |
74 | * Verify the arguments and make sure the filename is relative | |
75 | * to the base directory. | |
76 | */ | |
77 | 6 | if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0 |
78 | || !filename.startsWith( basedir ) ) | |
79 | { | |
80 | 4 | 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 | 2 | String separator = determineSeparator( filename ); |
89 | 2 | basedir = StringUtils.chompLast( basedir, separator ); |
90 | 2 | 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 | 2 | String relativeFilename = filename.substring( basedir.length() ); |
98 | ||
99 | 2 | 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 | 10 | if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) ) |
126 | { | |
127 | 3 | return ""; |
128 | } | |
129 | ||
130 | // normalise the path delimiters | |
131 | 7 | String fromPath = new File( oldPath ).getPath(); |
132 | 7 | String toPath = new File( newPath ).getPath(); |
133 | ||
134 | // strip any leading slashes if its a windows path | |
135 | 7 | if ( toPath.matches( "^\\[a-zA-Z]:" ) ) |
136 | { | |
137 | 0 | toPath = toPath.substring( 1 ); |
138 | } | |
139 | 7 | if ( fromPath.matches( "^\\[a-zA-Z]:" ) ) |
140 | { | |
141 | 0 | fromPath = fromPath.substring( 1 ); |
142 | } | |
143 | ||
144 | // lowercase windows drive letters. | |
145 | 7 | if ( fromPath.startsWith( ":", 1 ) ) |
146 | { | |
147 | 0 | fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 ); |
148 | } | |
149 | 7 | if ( toPath.startsWith( ":", 1 ) ) |
150 | { | |
151 | 0 | 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 | 7 | 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 | 0 | return null; |
162 | } | |
163 | ||
164 | 7 | 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 | 0 | return null; |
170 | } | |
171 | ||
172 | 7 | String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar ); |
173 | ||
174 | 7 | if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) ) |
175 | { | |
176 | 4 | return resultPath + File.separator; |
177 | } | |
178 | ||
179 | 3 | 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 | 2 | if ( filename.length() == 0 ) |
200 | { | |
201 | 0 | 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 | 2 | int slashCount = StringUtils.countMatches( filename, separator ) - 1; |
210 | 2 | if ( slashCount <= 0 ) |
211 | { | |
212 | 0 | 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 | 2 | StringBuilder sb = new StringBuilder(); |
221 | 5 | for ( int i = 0; i < slashCount; i++ ) |
222 | { | |
223 | 3 | sb.append( "../" ); |
224 | } | |
225 | ||
226 | /* | |
227 | * Finally, return the relative path but strip the trailing | |
228 | * slash to mimic Anakia's behavior. | |
229 | */ | |
230 | 2 | 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 | 2 | int forwardCount = StringUtils.countMatches( filename, "/" ); |
246 | 2 | int backwardCount = StringUtils.countMatches( filename, "\\" ); |
247 | ||
248 | 2 | 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 | 16 | if ( path == null ) |
260 | { | |
261 | 5 | return null; |
262 | } | |
263 | 11 | if ( path.length() >= 2 && path.charAt( 1 ) == ':' ) |
264 | { | |
265 | 2 | path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 ); |
266 | } | |
267 | 11 | 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 | 7 | StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); |
274 | 7 | StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); |
275 | ||
276 | 7 | int count = 0; |
277 | ||
278 | // walk along the to path looking for divergence from the from path | |
279 | 17 | while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() ) |
280 | { | |
281 | 12 | if ( separatorChar == '\\' ) |
282 | { | |
283 | 0 | if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) ) |
284 | { | |
285 | 0 | break; |
286 | } | |
287 | } | |
288 | else | |
289 | { | |
290 | 12 | if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) ) |
291 | { | |
292 | 2 | break; |
293 | } | |
294 | } | |
295 | ||
296 | 10 | count++; |
297 | } | |
298 | ||
299 | // reinitialise the tokenisers to count positions to retrieve the | |
300 | // gobbled token | |
301 | ||
302 | 7 | toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); |
303 | 7 | fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); |
304 | ||
305 | 17 | while ( count-- > 0 ) |
306 | { | |
307 | 10 | fromTokeniser.nextToken(); |
308 | 10 | toTokeniser.nextToken(); |
309 | } | |
310 | ||
311 | 7 | StringBuilder relativePath = new StringBuilder(); |
312 | ||
313 | // add back refs for the rest of from location. | |
314 | 15 | while ( fromTokeniser.hasMoreTokens() ) |
315 | { | |
316 | 8 | fromTokeniser.nextToken(); |
317 | ||
318 | 8 | relativePath.append( ".." ); |
319 | ||
320 | 8 | if ( fromTokeniser.hasMoreTokens() ) |
321 | { | |
322 | 4 | relativePath.append( separatorChar ); |
323 | } | |
324 | } | |
325 | ||
326 | 7 | if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() ) |
327 | { | |
328 | 2 | relativePath.append( separatorChar ); |
329 | } | |
330 | ||
331 | // add fwd fills for whatevers left of newPath. | |
332 | 17 | while ( toTokeniser.hasMoreTokens() ) |
333 | { | |
334 | 10 | relativePath.append( toTokeniser.nextToken() ); |
335 | ||
336 | 10 | if ( toTokeniser.hasMoreTokens() ) |
337 | { | |
338 | 5 | relativePath.append( separatorChar ); |
339 | } | |
340 | } | |
341 | 7 | return relativePath.toString(); |
342 | } | |
343 | } |