001package org.unix4j.util; 002 003import java.io.File; 004import java.util.List; 005 006/** 007 * This class represents a base directory for relative paths. The 008 * {@link #getRelativePathFor(File)} method returns a path string relative to 009 * the base directory passed to the constructor of this class. 010 */ 011public class RelativePathBase { 012 013 /** 014 * Settings used to construct relative paths. 015 */ 016 public static interface Settings { 017 /** 018 * Appends the path for a file that equals the base directory. Some 019 * settings may append "." or "./" and others the base directory name. 020 * 021 * @param result 022 * the string builder with the result path 023 * @param baseDir 024 * the base directory, here also the relative path target 025 * @return the {@code result} instance for chained invocations 026 */ 027 StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir); 028 029 /** 030 * Appends the path prefix for children of the base directory. Most 031 * settings add "./" as prefix, others maybe nothing and some add 032 * nothing for direct children but "./" for all other child paths. 033 * 034 * @param result 035 * the string builder with the result path 036 * @param baseDir 037 * the base directory, here a parent directory of the 038 * relative path target 039 * @return the {@code result} instance for chained invocations 040 */ 041 StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase); 042 043 /** 044 * Appends the path prefix from the base directory to the common 045 * ancestor of base directory and target file. Most settings add one 046 * "../" element for every directory up to the common ancestor. 047 * 048 * @param result 049 * the string builder with the result path 050 * @param baseDir 051 * the base directory, here a directory which has a common 052 * ancestor with the relative path target 053 * @return the {@code result} instance for chained invocations 054 */ 055 StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor); 056 } 057 058 /** 059 * Default settings to construct relative paths: "." represents the base 060 * directory, "./" is used as prefix of child paths (but "" for direct 061 * children) and "../" for elements up to the common ancestor. 062 */ 063 public static final Settings DEFAULT = new Settings() { 064 /** 065 * Appends "." to {@code result} representing the base directory. 066 */ 067 @Override 068 public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) { 069 return result.append('.'); 070 } 071 072 /** 073 * Appends "./" to {@code result} as prefix for child directories, but 074 * only if {@code directChildOfBase==false} (no prefix for direct 075 * children of the base directory). 076 */ 077 @Override 078 public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) { 079 if (directChildOfBase) { 080 return result; 081 } 082 return result.append("./"); 083 } 084 085 /** 086 * Appends one "../" element for every directory up to the common 087 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}). 088 */ 089 @Override 090 public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) { 091 for (int i = 0; i < levelsToCommonAncestor; i++) { 092 result.append("../"); 093 } 094 return result; 095 } 096 }; 097 /** 098 * Alternative settings to construct relative paths similar to 099 * {@link #DEFAULT}: "." represents the base directory, "./" is used as 100 * prefix of child paths and "../" for elements up to the common ancestor. 101 * As opposed to the DEFAULT settings, the "./" prefix is used for all child 102 * paths including direct children. 103 */ 104 public static final Settings ALL_CHILDREN_WITH_DOT_PREFIX = new Settings() { 105 /** 106 * Appends "." to {@code result} representing the base directory. 107 */ 108 @Override 109 public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) { 110 return result.append('.'); 111 } 112 113 /** 114 * Appends "./" to {@code result} as prefix for child directories, but 115 * only if {@code directChildOfBase==false} (no prefix for direct 116 * children of the base directory). 117 */ 118 @Override 119 public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) { 120 return result.append("./"); 121 } 122 123 /** 124 * Appends one "../" element for every directory up to the common 125 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}). 126 */ 127 @Override 128 public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) { 129 for (int i = 0; i < levelsToCommonAncestor; i++) { 130 result.append("../"); 131 } 132 return result; 133 } 134 }; 135 /** 136 * Alternative settings to construct relative paths without prefixes for 137 * children: "." represents the base directory, no prefix for child paths 138 * and "../" for elements up to the common ancestor. 139 */ 140 public static final Settings CHILDREN_WITHOUT_PREFIX = new Settings() { 141 /** 142 * Appends "." to {@code result} representing the base directory. 143 */ 144 @Override 145 public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) { 146 return result.append('.'); 147 } 148 149 /** 150 * Appends "./" to {@code result} as prefix for child directories, but 151 * only if {@code directChildOfBase==false} (no prefix for direct 152 * children of the base directory). 153 */ 154 @Override 155 public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) { 156 return result; 157 } 158 159 /** 160 * Appends one "../" element for every directory up to the common 161 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}). 162 */ 163 @Override 164 public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) { 165 for (int i = 0; i < levelsToCommonAncestor; i++) { 166 result.append("../"); 167 } 168 return result; 169 } 170 }; 171 172 private final File base; 173 private final Settings settings; 174 175 /** 176 * Constructor with base directory for relative paths using {@link #DEFAULT} 177 * settings to construct relative paths. 178 * 179 * @param base 180 * the basis for relative paths returned by 181 * {@link #getRelativePathFor(File)} 182 */ 183 public RelativePathBase(String base) { 184 this(new File(base)); 185 } 186 187 /** 188 * Constructor with base directory for relative paths using {@link #DEFAULT} 189 * settings to construct relative paths. 190 * 191 * @param base 192 * the basis for relative paths returned by 193 * {@link #getRelativePathFor(File)} 194 */ 195 public RelativePathBase(File base) { 196 this(base, DEFAULT); 197 } 198 199 /** 200 * Constructor with base directory for relative paths using the specified 201 * settings to construct relative paths. 202 * 203 * @param base 204 * the basis for relative paths returned by 205 * {@link #getRelativePathFor(File)} 206 * @param settings 207 * the settings used to construct relative paths 208 */ 209 public RelativePathBase(String base, Settings settings) { 210 this(new File(base), settings); 211 } 212 213 /** 214 * Constructor with base directory for relative paths using the specified 215 * settings to construct relative paths. 216 * 217 * @param base 218 * the basis for relative paths returned by 219 * {@link #getRelativePathFor(File)} 220 * @param settings 221 * the settings used to construct relative paths 222 */ 223 public RelativePathBase(File base, Settings settings) { 224 this.base = base; 225 this.settings = settings; 226 } 227 228 /** 229 * Returns the base directory for relative paths, the directory that was 230 * passed to the constructor. 231 */ 232 public File getBase() { 233 return base; 234 } 235 236 /** 237 * Returns the settings used to construct relative paths. Has been passed to 238 * the constructor unless {@link #DEFAULT} settings are used. 239 * 240 * @return the settings used to construct relative paths 241 */ 242 public Settings getSettings() { 243 return settings; 244 } 245 246 /** 247 * Returns the path of the given {@code file} relative to the 248 * {@link #getBase() base} directory. 249 * <p> 250 * The relative path is evaluated as follows: 251 * <ol> 252 * <li>If {@code base} is the same as {@code file}, "." is returned</li> 253 * <li>If {@code base} is the direct parent directory of {@code file}, the 254 * simple file name is returned</li> 255 * <li>If {@code base} is a non-direct parent directory of {@code file}, the 256 * relative path from {@code base} to {@code file} is returned</li> 257 * <li>If {@code base} is not parent directory of {@code file}, the relative 258 * path is the path from {@code base} to the common ancestor and then to 259 * {@code file}</li> 260 * <li>If {@code base} is not parent directory of {@code file} and the only 261 * common ancestor of the two is the base directory, the absolute path of 262 * {@code file} is returned</li> 263 * </ol> 264 * Examples (same order as above): 265 * <ol> 266 * <li>("/home/john", "/home/john") → "."</li> 267 * <li>("/home/john", "/home/john/notes.txt") → "notes.txt"</li> 268 * <li>("/home/john", "/home/john/documents/important") → 269 * "./documents/important"</li> 270 * <li>("/home/john", "/home/smith/public/holidays.pdf") → 271 * "../smith/public/holidays.pdf"</li> 272 * <li>("/home/john", "/var/tmp/test.out") → "/var/tmp/test.out"</li> 273 * </ol> 274 * 275 * @param file 276 * the target file whose relative path is returned 277 * @return the path of {@code file} relative to the {@link #getBase() base} 278 * directory 279 */ 280 public String getRelativePathFor(File file) { 281 final StringBuilder path = new StringBuilder(); 282 if (base.equals(file)) { 283 return settings.appendPathForBaseDir(path, file).toString(); 284 } 285 if (base.equals(file.getParentFile())) { 286 return settings.appendPrefixForChildren(path, base, true).append(file.getName()).toString(); 287 } 288 final List<String> baseParts = FileUtil.getPathElements(base); 289 final List<String> fileParts = FileUtil.getPathElements(file); 290 291 final int len = Math.min(baseParts.size(), fileParts.size()); 292 int common = 0; 293 while (common < len && baseParts.get(common).equals(fileParts.get(common))) { 294 common++; 295 } 296 if (common == 0) { 297 return file.getAbsolutePath().replace('\\', '/'); 298 } 299 if (common < baseParts.size()) { 300 settings.appendPrefixToCommonAncestor(path, base, baseParts.size() - common); 301 } else { 302 settings.appendPrefixForChildren(path, base, false); 303 } 304 for (int i = common; i < fileParts.size(); i++) { 305 path.append(fileParts.get(i)).append('/'); 306 } 307 // cut off last '/' and return string 308 return path.deleteCharAt(path.length() - 1).toString(); 309 } 310 311 /** 312 * Returns the path of the given {@code file} relative to the 313 * {@link #getBase() base} directory. 314 * <p> 315 * The relative path is evaluated as follows: 316 * <ol> 317 * <li>If {@code base} is the same as {@code file}, "." is returned</li> 318 * <li>If {@code base} is the direct parent directory of {@code file}, the 319 * simple file name is returned</li> 320 * <li>If {@code base} is a non-direct parent directory of {@code file}, the 321 * relative path from {@code base} to {@code file} is returned</li> 322 * <li>If {@code base} is not parent directory of {@code file}, the relative 323 * path is the path from {@code base} to the common ancestor and then to 324 * {@code file}</li> 325 * <li>If {@code base} is not parent directory of {@code file} and the only 326 * common ancestor of the two is the base directory, the absolute path of 327 * {@code file} is returned</li> 328 * </ol> 329 * Examples (same order as above): 330 * <ol> 331 * <li>("/home/john", "/home/john") → "."</li> 332 * <li>("/home/john", "/home/john/notes.txt") → "notes.txt"</li> 333 * <li>("/home/john", "/home/john/documents/important") → 334 * "./documents/important"</li> 335 * <li>("/home/john", "/home/smith/public/holidays.pdf") → 336 * "../smith/public/holidays.pdf"</li> 337 * <li>("/home/john", "/var/tmp/test.out") → "/var/tmp/test.out"</li> 338 * </ol> 339 * 340 * @param file 341 * the target file whose relative path is returned 342 * @return the path of {@code file} relative to the {@link #getBase() base} 343 * directory 344 */ 345 public String getRelativePathFor(String file) { 346 return getRelativePathFor(new File(file)); 347 } 348}