1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. 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, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.maven.shared.utils; 20 21 import javax.annotation.Nonnull; 22 import javax.annotation.Nullable; 23 24 import java.io.File; 25 import java.util.StringTokenizer; 26 27 /** 28 * <p>Path tool contains static methods to assist in determining path-related 29 * information such as relative paths.</p> 30 * <p> 31 * This class originally got developed at Apache Anakia and later maintained 32 * in maven-utils of Apache Maven-1. 33 * Some external fixes by Apache Committers have been applied later. 34 * </p> 35 */ 36 public class PathTool { 37 38 /** 39 * The constructor. 40 * 41 * @deprecated This is a utility class with only static methods. Don't create instances of it. 42 */ 43 @Deprecated 44 public PathTool() {} 45 46 /** 47 * Determines the relative path of a filename from a base directory. 48 * This method is useful in building relative links within pages of 49 * a web site. It provides similar functionality to Anakia's 50 * <code>$relativePath</code> context variable. The arguments to 51 * this method may contain either forward or backward slashes as 52 * file separators. The relative path returned is formed using 53 * forward slashes as it is expected this path is to be used as a 54 * link in a web page (again mimicking Anakia's behavior). 55 * <p> 56 * This method is thread-safe. 57 * </p> 58 * <pre> 59 * PathTool.getRelativePath( null, null ) = "" 60 * PathTool.getRelativePath( null, "/usr/local/java/bin" ) = "" 61 * PathTool.getRelativePath( "/usr/local/", null ) = "" 62 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" ) = ".." 63 * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.." 64 * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "" 65 * </pre> 66 * 67 * @param basedir The base directory. 68 * @param filename The filename that is relative to the base 69 * directory. 70 * @return The relative path of the filename from the base 71 * directory. This value is not terminated with a forward slash. 72 * A zero-length string is returned if: the filename is not relative to 73 * the base directory, <code>basedir</code> is null or zero-length, 74 * or <code>filename</code> is null or zero-length. 75 */ 76 public static String getRelativePath(@Nullable String basedir, @Nullable String filename) { 77 basedir = uppercaseDrive(basedir); 78 filename = uppercaseDrive(filename); 79 80 /* 81 * Verify the arguments and make sure the filename is relative 82 * to the base directory. 83 */ 84 if (basedir == null 85 || basedir.length() == 0 86 || filename == null 87 || filename.length() == 0 88 || !filename.startsWith(basedir)) { 89 return ""; 90 } 91 92 /* 93 * Normalize the arguments. First, determine the file separator 94 * that is being used, then strip that off the end of both the 95 * base directory and filename. 96 */ 97 String separator = determineSeparator(filename); 98 basedir = StringUtils.chompLast(basedir, separator); 99 filename = StringUtils.chompLast(filename, separator); 100 101 /* 102 * Remove the base directory from the filename to end up with a 103 * relative filename (relative to the base directory). This 104 * filename is then used to determine the relative path. 105 */ 106 String relativeFilename = filename.substring(basedir.length()); 107 108 return determineRelativePath(relativeFilename, separator); 109 } 110 111 /** 112 * <p>This method can calculate the relative path between two paths on a file system.</p> 113 * <pre> 114 * PathTool.getRelativeFilePath( null, null ) = "" 115 * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = "" 116 * PathTool.getRelativeFilePath( "/usr/local", null ) = "" 117 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin" 118 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin" 119 * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.." 120 * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh" 121 * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.." 122 * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin" 123 * PathTool.getRelativeFilePath( "/bin", "/usr/local/" ) = "../usr/local" 124 * </pre> 125 * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character. 126 * 127 * @param oldPath old path 128 * @param newPath new path 129 * @return a relative file path from <code>oldPath</code>. 130 */ 131 public static String getRelativeFilePath(final String oldPath, final String newPath) { 132 if (StringUtils.isEmpty(oldPath) || StringUtils.isEmpty(newPath)) { 133 return ""; 134 } 135 136 // normalise the path delimiters 137 String fromPath = new File(oldPath).getPath(); 138 String toPath = new File(newPath).getPath(); 139 140 // strip any leading slashes if its a windows path 141 if (toPath.matches("^\\[a-zA-Z]:")) { 142 toPath = toPath.substring(1); 143 } 144 if (fromPath.matches("^\\[a-zA-Z]:")) { 145 fromPath = fromPath.substring(1); 146 } 147 148 // lowercase windows drive letters. 149 if (fromPath.startsWith(":", 1)) { 150 fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1); 151 } 152 if (toPath.startsWith(":", 1)) { 153 toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1); 154 } 155 156 // check for the presence of windows drives. No relative way of 157 // traversing from one to the other. 158 if ((toPath.startsWith(":", 1) && fromPath.startsWith(":", 1)) 159 && (!toPath.substring(0, 1).equals(fromPath.substring(0, 1)))) { 160 // they both have drive path element but they dont match, no 161 // relative path 162 return null; 163 } 164 165 if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1)) 166 || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) { 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 return resultPath + File.separator; 176 } 177 178 return resultPath; 179 } 180 181 // ---------------------------------------------------------------------- 182 // Private methods 183 // ---------------------------------------------------------------------- 184 185 /** 186 * Determines the relative path of a filename. For each separator 187 * within the filename (except the leading if present), append the 188 * "../" string to the return value. 189 * 190 * @param filename The filename to parse. 191 * @param separator The separator used within the filename. 192 * @return The relative path of the filename. This value is not 193 * terminated with a forward slash. A zero-length string is 194 * returned if: the filename is zero-length. 195 */ 196 @Nonnull 197 private static String determineRelativePath(@Nonnull String filename, @Nonnull String separator) { 198 if (filename.length() == 0) { 199 return ""; 200 } 201 202 /* 203 * Count the slashes in the relative filename, but exclude the 204 * leading slash. If the path has no slashes, then the filename 205 * is relative to the current directory. 206 */ 207 int slashCount = StringUtils.countMatches(filename, separator) - 1; 208 if (slashCount <= 0) { 209 return "."; 210 } 211 212 /* 213 * The relative filename contains one or more slashes indicating 214 * that the file is within one or more directories. Thus, each 215 * slash represents a "../" in the relative path. 216 */ 217 StringBuilder sb = new StringBuilder(); 218 for (int i = 0; i < slashCount; i++) { 219 sb.append("../"); 220 } 221 222 /* 223 * Finally, return the relative path but strip the trailing 224 * slash to mimic Anakia's behavior. 225 */ 226 return StringUtils.chop(sb.toString()); 227 } 228 229 /** 230 * Helper method to determine the file separator (forward or 231 * backward slash) used in a filename. The slash that occurs more 232 * often is returned as the separator. 233 * 234 * @param filename The filename parsed to determine the file 235 * separator. 236 * @return The file separator used within <code>filename</code>. 237 * This value is either a forward or backward slash. 238 */ 239 private static String determineSeparator(String filename) { 240 int forwardCount = StringUtils.countMatches(filename, "/"); 241 int backwardCount = StringUtils.countMatches(filename, "\\"); 242 243 return forwardCount >= backwardCount ? "/" : "\\"; 244 } 245 246 /** 247 * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase 248 * 249 * @param path old path 250 * @return String 251 */ 252 static String uppercaseDrive(@Nullable String path) { 253 if (path == null) { 254 return null; 255 } 256 if (path.length() >= 2 && path.charAt(1) == ':') { 257 path = Character.toUpperCase(path.charAt(0)) + path.substring(1); 258 } 259 return path; 260 } 261 262 @Nonnull 263 private static String buildRelativePath( 264 @Nonnull String toPath, @Nonnull String fromPath, final char separatorChar) { 265 // use tokeniser to traverse paths and for lazy checking 266 StringTokenizer toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar)); 267 StringTokenizer fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar)); 268 269 int count = 0; 270 271 // walk along the to path looking for divergence from the from path 272 while (toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens()) { 273 if (separatorChar == '\\') { 274 if (!fromTokeniser.nextToken().equalsIgnoreCase(toTokeniser.nextToken())) { 275 break; 276 } 277 } else { 278 if (!fromTokeniser.nextToken().equals(toTokeniser.nextToken())) { 279 break; 280 } 281 } 282 283 count++; 284 } 285 286 // reinitialise the tokenisers to count positions to retrieve the 287 // gobbled token 288 289 toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar)); 290 fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar)); 291 292 while (count-- > 0) { 293 fromTokeniser.nextToken(); 294 toTokeniser.nextToken(); 295 } 296 297 StringBuilder relativePath = new StringBuilder(); 298 299 // add back refs for the rest of from location. 300 while (fromTokeniser.hasMoreTokens()) { 301 fromTokeniser.nextToken(); 302 303 relativePath.append(".."); 304 305 if (fromTokeniser.hasMoreTokens()) { 306 relativePath.append(separatorChar); 307 } 308 } 309 310 if (relativePath.length() != 0 && toTokeniser.hasMoreTokens()) { 311 relativePath.append(separatorChar); 312 } 313 314 // add fwd fills for whatevers left of newPath. 315 while (toTokeniser.hasMoreTokens()) { 316 relativePath.append(toTokeniser.nextToken()); 317 318 if (toTokeniser.hasMoreTokens()) { 319 relativePath.append(separatorChar); 320 } 321 } 322 return relativePath.toString(); 323 } 324 }