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 }