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