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 /** 40 * The constructor. 41 * 42 * @deprecated This is a utility class with only static methods. Don't create instances of it. 43 */ 44 @Deprecated 45 public PathTool() 46 { 47 } 48 49 /** 50 * Determines the relative path of a filename from a base directory. 51 * This method is useful in building relative links within pages of 52 * a web site. It provides similar functionality to Anakia's 53 * <code>$relativePath</code> context variable. The arguments to 54 * this method may contain either forward or backward slashes as 55 * file separators. The relative path returned is formed using 56 * forward slashes as it is expected this path is to be used as a 57 * link in a web page (again mimicking Anakia's behavior). 58 * <p/> 59 * This method is thread-safe. 60 * <br/> 61 * <pre> 62 * PathTool.getRelativePath( null, null ) = "" 63 * PathTool.getRelativePath( null, "/usr/local/java/bin" ) = "" 64 * PathTool.getRelativePath( "/usr/local/", null ) = "" 65 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" ) = ".." 66 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.." 67 * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "" 68 * </pre> 69 * 70 * @param basedir The base directory. 71 * @param filename The filename that is relative to the base 72 * directory. 73 * @return The relative path of the filename from the base 74 * directory. This value is not terminated with a forward slash. 75 * A zero-length string is returned if: the filename is not relative to 76 * the base directory, <code>basedir</code> is null or zero-length, 77 * or <code>filename</code> is null or zero-length. 78 */ 79 public static String getRelativePath( @Nullable String basedir, @Nullable String filename ) 80 { 81 basedir = uppercaseDrive( basedir ); 82 filename = uppercaseDrive( filename ); 83 84 /* 85 * Verify the arguments and make sure the filename is relative 86 * to the base directory. 87 */ 88 if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0 89 || !filename.startsWith( basedir ) ) 90 { 91 return ""; 92 } 93 94 /* 95 * Normalize the arguments. First, determine the file separator 96 * that is being used, then strip that off the end of both the 97 * base directory and filename. 98 */ 99 String separator = determineSeparator( filename ); 100 basedir = StringUtils.chompLast( basedir, separator ); 101 filename = StringUtils.chompLast( filename, separator ); 102 103 /* 104 * Remove the base directory from the filename to end up with a 105 * relative filename (relative to the base directory). This 106 * filename is then used to determine the relative path. 107 */ 108 String relativeFilename = filename.substring( basedir.length() ); 109 110 return determineRelativePath( relativeFilename, separator ); 111 } 112 113 /** 114 * This method can calculate the relative path between two pathes on a file system. 115 * <br/> 116 * <pre> 117 * PathTool.getRelativeFilePath( null, null ) = "" 118 * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = "" 119 * PathTool.getRelativeFilePath( "/usr/local", null ) = "" 120 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin" 121 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin" 122 * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.." 123 * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh" 124 * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.." 125 * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin" 126 * PathTool.getRelativeFilePath( "/bin", "/usr/local/" ) = "../usr/local" 127 * </pre> 128 * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character. 129 * 130 * @param oldPath old path 131 * @param newPath new path 132 * @return a relative file path from <code>oldPath</code>. 133 */ 134 public static String getRelativeFilePath( final String oldPath, final String newPath ) 135 { 136 if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) ) 137 { 138 return ""; 139 } 140 141 // normalise the path delimiters 142 String fromPath = new File( oldPath ).getPath(); 143 String toPath = new File( newPath ).getPath(); 144 145 // strip any leading slashes if its a windows path 146 if ( toPath.matches( "^\\[a-zA-Z]:" ) ) 147 { 148 toPath = toPath.substring( 1 ); 149 } 150 if ( fromPath.matches( "^\\[a-zA-Z]:" ) ) 151 { 152 fromPath = fromPath.substring( 1 ); 153 } 154 155 // lowercase windows drive letters. 156 if ( fromPath.startsWith( ":", 1 ) ) 157 { 158 fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 ); 159 } 160 if ( toPath.startsWith( ":", 1 ) ) 161 { 162 toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 ); 163 } 164 165 // check for the presence of windows drives. No relative way of 166 // traversing from one to the other. 167 if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) 168 && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) ) 169 { 170 // they both have drive path element but they dont match, no 171 // relative path 172 return null; 173 } 174 175 if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) ) 176 || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) ) 177 { 178 // one has a drive path element and the other doesnt, no relative 179 // path. 180 return null; 181 } 182 183 String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar ); 184 185 if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) ) 186 { 187 return resultPath + File.separator; 188 } 189 190 return resultPath; 191 } 192 193 // ---------------------------------------------------------------------- 194 // Private methods 195 // ---------------------------------------------------------------------- 196 197 /** 198 * Determines the relative path of a filename. For each separator 199 * within the filename (except the leading if present), append the 200 * "../" string to the return value. 201 * 202 * @param filename The filename to parse. 203 * @param separator The separator used within the filename. 204 * @return The relative path of the filename. This value is not 205 * terminated with a forward slash. A zero-length string is 206 * returned if: the filename is zero-length. 207 */ 208 @Nonnull private static String determineRelativePath( @Nonnull String filename, @Nonnull String separator ) 209 { 210 if ( filename.length() == 0 ) 211 { 212 return ""; 213 } 214 215 /* 216 * Count the slashes in the relative filename, but exclude the 217 * leading slash. If the path has no slashes, then the filename 218 * is relative to the current directory. 219 */ 220 int slashCount = StringUtils.countMatches( filename, separator ) - 1; 221 if ( slashCount <= 0 ) 222 { 223 return "."; 224 } 225 226 /* 227 * The relative filename contains one or more slashes indicating 228 * that the file is within one or more directories. Thus, each 229 * slash represents a "../" in the relative path. 230 */ 231 StringBuilder sb = new StringBuilder(); 232 for ( int i = 0; i < slashCount; i++ ) 233 { 234 sb.append( "../" ); 235 } 236 237 /* 238 * Finally, return the relative path but strip the trailing 239 * slash to mimic Anakia's behavior. 240 */ 241 return StringUtils.chop( sb.toString() ); 242 } 243 244 /** 245 * Helper method to determine the file separator (forward or 246 * backward slash) used in a filename. The slash that occurs more 247 * often is returned as the separator. 248 * 249 * @param filename The filename parsed to determine the file 250 * separator. 251 * @return The file separator used within <code>filename</code>. 252 * This value is either a forward or backward slash. 253 */ 254 private static String determineSeparator( String filename ) 255 { 256 int forwardCount = StringUtils.countMatches( filename, "/" ); 257 int backwardCount = StringUtils.countMatches( filename, "\\" ); 258 259 return forwardCount >= backwardCount ? "/" : "\\"; 260 } 261 262 /** 263 * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase 264 * 265 * @param path old path 266 * @return String 267 */ 268 static String uppercaseDrive( @Nullable String path ) 269 { 270 if ( path == null ) 271 { 272 return null; 273 } 274 if ( path.length() >= 2 && path.charAt( 1 ) == ':' ) 275 { 276 path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 ); 277 } 278 return path; 279 } 280 281 @Nonnull private static String buildRelativePath( @Nonnull String toPath, @Nonnull String fromPath, 282 final char separatorChar ) 283 { 284 // use tokeniser to traverse paths and for lazy checking 285 StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); 286 StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); 287 288 int count = 0; 289 290 // walk along the to path looking for divergence from the from path 291 while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() ) 292 { 293 if ( separatorChar == '\\' ) 294 { 295 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) ) 296 { 297 break; 298 } 299 } 300 else 301 { 302 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) ) 303 { 304 break; 305 } 306 } 307 308 count++; 309 } 310 311 // reinitialise the tokenisers to count positions to retrieve the 312 // gobbled token 313 314 toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); 315 fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); 316 317 while ( count-- > 0 ) 318 { 319 fromTokeniser.nextToken(); 320 toTokeniser.nextToken(); 321 } 322 323 StringBuilder relativePath = new StringBuilder(); 324 325 // add back refs for the rest of from location. 326 while ( fromTokeniser.hasMoreTokens() ) 327 { 328 fromTokeniser.nextToken(); 329 330 relativePath.append( ".." ); 331 332 if ( fromTokeniser.hasMoreTokens() ) 333 { 334 relativePath.append( separatorChar ); 335 } 336 } 337 338 if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() ) 339 { 340 relativePath.append( separatorChar ); 341 } 342 343 // add fwd fills for whatevers left of newPath. 344 while ( toTokeniser.hasMoreTokens() ) 345 { 346 relativePath.append( toTokeniser.nextToken() ); 347 348 if ( toTokeniser.hasMoreTokens() ) 349 { 350 relativePath.append( separatorChar ); 351 } 352 } 353 return relativePath.toString(); 354 } 355 }