001package org.unix4j.util; 002 003import java.io.File; 004import java.io.FilenameFilter; 005import java.net.URL; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.LinkedList; 009import java.util.List; 010import java.util.regex.Pattern; 011 012import org.unix4j.variable.Arg; 013 014/** 015 * Utility class with static methods involving files. 016 */ 017public class FileUtil { 018 019 public static final String ROOT_UNIX = "/"; 020 021 // absolute prefix also: \\\\ i.e. actually \\ for network drive 022 public static final String ROOT_WINDOWS = "C:\\"; 023 public static final String ROOT_WINDOWS_NETWORK = "\\\\"; 024 025 public static final String ROOT = isWindows() ? ROOT_WINDOWS : ROOT_UNIX; 026 027 private static boolean isWindows() { 028 return OS.Windows.equals(OS.current()); 029 } 030 031 /** 032 * Returns the user's current working directory taken from the system 033 * property "user.dir". 034 * 035 * @return the user's current working directory 036 * @see System#getProperties() 037 */ 038 public static File getUserDir() { 039 return new File(System.getProperty("user.dir")); 040 } 041 042 /** 043 * Returns the specified files in a new mutable array list. This is similar 044 * to {@link Arrays#asList(Object...)} but the returned list is expandable. 045 * 046 * @param files 047 * the files to add to a new list 048 * @return a new array list containing the specified files 049 */ 050 public static List<File> toList(File... files) { 051 final List<File> list = new ArrayList<File>(files.length + 2); 052 for (int i = 0; i < files.length; i++) { 053 list.add(files[i]); 054 } 055 return list; 056 } 057 058 /** 059 * Returns the path of the given {@code file} relative to the given 060 * {@code root}. 061 * <p> 062 * The relative path is evaluated as follows: 063 * <ol> 064 * <li>If {@code root} is the same as {@code file}, "." is returned</li> 065 * <li>If {@code root} is the direct parent directory of {@code file}, the 066 * simple file name is returned</li> 067 * <li>If {@code root} is a non-direct parent directory of {@code file}, the 068 * relative path from {@code root} to {@code file} is returned</li> 069 * <li>If {@code root} is not parent directory of {@code file}, the relative 070 * path is the path from {@code root} to the common ancestor and then to 071 * {@code file}</li> 072 * <li>If {@code root} is not parent directory of {@code file} and the only 073 * common ancestor of the two is the root directory, the absolute path of 074 * {@code file} is returned</li> 075 * </ol> 076 * Examples (same order as above): 077 * <ol> 078 * <li>("/home/john", "/home/john") → "."</li> 079 * <li>("/home/john", "/home/john/notes.txt") → "notes.txt"</li> 080 * <li>("/home/john", "/home/john/documents/important") → 081 * "./documents/important"</li> 082 * <li>("/home/john", "/home/smith/public/holidays.pdf") → 083 * "../smith/public/holidays.pdf"</li> 084 * <li>("/home/john", "/var/tmp/test.out") → "/var/tmp/test.out"</li> 085 * </ol> 086 * 087 * @param root 088 * the root directory for the relative path 089 * @param file 090 * the file whose path should be returned 091 * @return the path of {@code file} relative to {@code root} 092 */ 093 public static String getRelativePath(File root, File file) { 094 return new RelativePathBase(root).getRelativePathFor(file); 095 } 096 097 /** 098 * Returns an absolute file for the given input file, resolving relative 099 * paths on the basis of the given {@code currentDirectory}. If the given 100 * {@code file} represents an absolute path or a variable, it is returned 101 * unchanged. If {@code currentDirectory==null}, 102 * {@link File#getAbsoluteFile()} is returned. 103 * 104 * @param currentDirectory 105 * the current directory 106 * @param file 107 * the file to be returned as absolute file 108 * @return the absolute path version of the given file with 109 * {@code currentDirectory} as basis for relative paths 110 * @see File#isAbsolute() 111 * @see Arg#isVariable(String) 112 * @see File#getAbsoluteFile() 113 */ 114 public static File toAbsoluteFile(File currentDirectory, File file) { 115 if (file.isAbsolute() || Arg.isVariable(file.getPath())) { 116 return file; 117 } 118 if (currentDirectory == null) { 119 return file.getAbsoluteFile(); 120 } 121 return new File(currentDirectory, file.getPath()).getAbsoluteFile(); 122 } 123 124 /** 125 * Returns all path elements of the given {@code file}. The absolute path of 126 * the file is used to evaluate the path elements. 127 * <p> 128 * For instance, a list with the 3 elements "var", "tmp", "out.txt" is 129 * returned for an input file "/var/tmp/out.txt". 130 * 131 * @param file 132 * the file whose path elements should be returned 133 * @return the path elements of {@code file} 134 */ 135 public static List<String> getPathElements(File file) { 136 file = file.getAbsoluteFile(); 137 final List<String> elements = new LinkedList<String>(); 138 do { 139 elements.add(0, file.getName()); 140 file = file.getParentFile(); 141 } while (file != null); 142 elements.remove(0); 143 return elements; 144 } 145 146 /** 147 * Expands files if necessary, meaning that input files with wildcards are 148 * expanded. If the specified {@code files} list contains no wildcard, the 149 * files are simply returned; all wildcard files are expanded. 150 * 151 * @param paths 152 * the file paths, possibly containing wildcard parts 153 * @return the expanded files resolving wildcards 154 */ 155 public static List<File> expandFiles(String... paths) { 156 return expandFiles(FileUtil.getUserDir(), paths); 157 } 158 159 /** 160 * Expands files if necessary, meaning that input files with wildcards are 161 * expanded. If the specified {@code files} list contains no wildcard, the 162 * files are simply returned; all wildcard files are expanded. The given 163 * current directory serves as basis for all relative paths. 164 * 165 * @param currentDirectory 166 * the basis for all relative paths 167 * @param paths 168 * the file paths, possibly containing wildcard parts 169 * @return the expanded files resolving wildcards 170 */ 171 public static List<File> expandFiles(File currentDirectory, String... paths) { 172 final List<File> expanded = new ArrayList<File>(paths.length); 173 for (final String path : paths) { 174 addFileExpanded(currentDirectory, new File(path), expanded); 175 } 176 return expanded; 177 } 178 179 private static void addFileExpanded(File currentDirectory, File file, 180 List<File> expandedFiles) { 181 if (!file.isAbsolute()) { 182 file = new File(currentDirectory, file.getPath()); 183 } 184 if (isWildcardFileName(file.getPath())) { 185 final List<String> parts = new LinkedList<String>(); 186 File f = file; 187 File p; 188 do { 189 parts.add(0, f.getName()); 190 p = f; 191 f = f.getParentFile(); 192 } while (f != null); 193 if (p.isDirectory()) { 194 // we pass p (the first directory) as starting directory 195 parts.remove(0); 196 } else if (p.getPath().endsWith("\\")) { 197 // must be a drive, such as \\mydrive\bla with parts {"", 198 // "mydrive", "bla"} 199 parts.remove(0); 200 } 201 202 // descend again until first wildcard part is found 203 while (!parts.isEmpty() && !isWildcardFileName(parts.get(0))) { 204 p = new File(p, parts.remove(0)); 205 } 206 if (!p.isDirectory()) { 207 // what now? throw exception? trace error? 208 throw new IllegalArgumentException("file not found: " + file 209 + " [root=" + p + ", currentDirectory=" 210 + currentDirectory + "]"); 211 } 212 listFiles(p, parts, expandedFiles); 213 } else { 214 if (file.exists()) { 215 expandedFiles.add(file); 216 } else { 217 // try file as relative path 218 final File relFile = new File(currentDirectory, file.getPath()); 219 if (relFile.exists()) { 220 expandedFiles.add(relFile); 221 } else { 222 // what now? throw exception? trace error? 223 throw new IllegalArgumentException("file not found: " 224 + file + " [currentDirectory=" + currentDirectory 225 + "]"); 226 } 227 } 228 } 229 } 230 231 private static void listFiles(File file, List<String> parts, List<File> dest) { 232 final String part = parts.remove(0); 233 final FilenameFilter filter = getFileNameFilter(part); 234 for (final File f : file.listFiles(filter)) { 235 if (parts.isEmpty()) { 236 dest.add(f); 237 } else { 238 if (f.isDirectory()) { 239 listFiles(f, parts, dest); 240 } 241 } 242 } 243 parts.add(0, part); 244 } 245 246 /** 247 * Returns a file name filter for the specified name. The name should be 248 * either a simple file name (not a path) or a wildcard expression such as 249 * "*" or "*.java". 250 * <p> 251 * The wildcards "*" and "?" are supported. "*" stands for any character 252 * repeated 0 to many times, "?" for exactly one arbitrary character. Both 253 * characters can be escaped with a preceding backslash character \ (Unix 254 * and MAC) or % character (Windows). 255 * 256 * @param name 257 * the name or pattern without path 258 * @return a file name filter matching either the name or the pattern 259 */ 260 public static FilenameFilter getFileNameFilter(String name) { 261 if (isWildcardFileName(name)) { 262 if (isWindows()) { 263 // escape char is % 264 name = name.replace("%%", "%_"); 265 name = name.replace("%.", "%/").replace(".", "\\.") 266 .replace("%/", "\\."); 267 name = name.replace("%*", "%/").replace("*", ".*") 268 .replace("%/", "\\*"); 269 name = name.replace("%?", "%/").replace("?", ".") 270 .replace("%/", "\\?"); 271 name = name.replace("%_", "%"); 272 } else { 273 // escape char is \ 274 name = name.replace("\\\\", "\\_"); 275 name = name.replace("\\.", "\\/").replace(".", "\\.") 276 .replace("\\/", "\\."); 277 name = name.replace("\\*", "\\/").replace("*", ".*") 278 .replace("\\/", "\\*"); 279 name = name.replace("\\?", "\\/").replace("?", ".") 280 .replace("\\/", "\\?"); 281 name = name.replace("\\_", "\\\\"); 282 } 283 final Pattern pattern = Pattern.compile(name); 284 return new FilenameFilter() { 285 @Override 286 public boolean accept(File dir, String name) { 287 return pattern.matcher(name).matches(); 288 } 289 }; 290 } else { 291 final String fileName = name; 292 return new FilenameFilter() { 293 @Override 294 public boolean accept(File dir, String name) { 295 return fileName.equals(name); 296 } 297 }; 298 } 299 } 300 301 /** 302 * Returns true if the given name or path contains unescaped wildcard 303 * characters. The characters "*" and "?" are considered wildcard chars if 304 * they are not escaped with a preceding backslash character \ (Unix and 305 * MAC) or % character (Windows). 306 * 307 * @param name 308 * the name or path 309 * @return true if the name contains unescaped wildcard characters 310 */ 311 public static boolean isWildcardFileName(String name) { 312 if (name.contains("*") || name.contains("?")) { 313 final String unescaped; 314 if (isWindows()) { 315 unescaped = name.replace("%%", "%_").replace("%*", "%_") 316 .replace("%?", "%_"); 317 } else { 318 unescaped = name.replace("\\\\", "\\_").replace("\\*", "\\_") 319 .replace("\\?", "\\_"); 320 } 321 return unescaped.contains("*") || unescaped.contains("?"); 322 } 323 return false; 324 } 325 326 /** 327 * This method returns the output directory of a given class. 328 * 329 * Example: Given a class: com.abc.def.MyClass Which is outputted to a 330 * directory: /home/ben/myproject/target/com/abc/dev/MyClass.class This 331 * method will return: /home/ben/myproject/target 332 * 333 * @param clazz 334 * @return A file representing the output directory containing the class and 335 * parent packages 336 */ 337 338 public static File getOutputDirectoryGivenClass(final Class<?> clazz) { 339 File parentDir = getDirectoryOfClassFile(clazz); 340 final int packageDepth = clazz.getName().split("\\.").length; 341 for (int i = 0; i < (packageDepth - 1); i++) { 342 parentDir = parentDir.getParentFile(); 343 } 344 return parentDir; 345 } 346 347 /** 348 * This method returns the parent directory of a given class. 349 * 350 * Example: Given a class: com.abc.def.MyClass Which is outputted to a 351 * directory: /home/ben/myproject/target/com/abc/def/MyClass.class This 352 * method will return: /home/ben/myproject/target/com/abc/def 353 * 354 * @param clazz 355 * @return A file representing the output directory containing the class and 356 * parent packages 357 */ 358 public static File getDirectoryOfClassFile(Class<?> clazz) { 359 final String resource = "/" + clazz.getName().replace(".", "/") 360 + ".class"; 361 final URL classFileURL = clazz.getResource(resource); 362 final File classFile = new File(classFileURL.getFile()); 363 return classFile.getParentFile(); 364 } 365 366 /** 367 * This method returns the parent directory of a given class. 368 * 369 * Example: Given a class: com.abc.def.MyClass Which is outputted to a 370 * directory: /home/ben/myproject/target/com/abc/def/MyClass.class This 371 * method will return: /home/ben/myproject/target/com/abc/def 372 * 373 * @param className 374 * fully qualified class name 375 * @return A file representing the output directory containing the class and 376 * parent packages 377 */ 378 public static File getDirectoryOfClassFile(final String className) { 379 final String resource = "/" + className.replace(".", "/") + ".class"; 380 final URL classFileURL = FileUtil.class.getResource(resource); 381 final File classFile = new File(classFileURL.getFile()); 382 return classFile.getParentFile(); 383 } 384 385 // no instances 386 private FileUtil() { 387 super(); 388 } 389}