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 }