001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.lib.commons.filesystem; 018 019import java.io.BufferedInputStream; 020import java.io.ByteArrayInputStream; 021import java.io.ByteArrayOutputStream; 022import java.io.Closeable; 023import java.io.EOFException; 024import java.io.File; 025import java.io.FileFilter; 026import java.io.FileInputStream; 027import java.io.FileOutputStream; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.OutputStream; 032import java.io.OutputStreamWriter; 033import java.io.Reader; 034import java.net.URI; 035import java.net.URISyntaxException; 036import java.net.URL; 037import java.nio.ByteBuffer; 038import java.nio.channels.FileChannel; 039import java.nio.charset.Charset; 040import java.nio.charset.StandardCharsets; 041import java.nio.file.Files; 042import java.nio.file.Paths; 043import java.util.ArrayList; 044import java.util.Arrays; 045import java.util.Collection; 046import java.util.Enumeration; 047import java.util.HashSet; 048import java.util.IllegalFormatException; 049import java.util.List; 050import java.util.Properties; 051import java.util.Set; 052import java.util.jar.JarEntry; 053import java.util.jar.JarFile; 054import java.util.jar.JarOutputStream; 055import java.util.regex.Pattern; 056import java.util.stream.Collectors; 057import java.util.zip.GZIPInputStream; 058import java.util.zip.ZipEntry; 059import java.util.zip.ZipInputStream; 060 061import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 062import org.conqat.lib.commons.assertion.CCSMAssert; 063import org.conqat.lib.commons.collections.CaseInsensitiveStringSet; 064import org.conqat.lib.commons.collections.CollectionUtils; 065import org.conqat.lib.commons.logging.ILogger; 066import org.conqat.lib.commons.resources.Resource; 067import org.conqat.lib.commons.string.StringUtils; 068 069/** 070 * File system utilities. 071 */ 072public class FileSystemUtils { 073 074 /** Encoding for UTF-8. */ 075 public static final String UTF8_ENCODING = StandardCharsets.UTF_8.name(); 076 077 /** The path to the directory used by Java to store temporary files */ 078 public static final String TEMP_DIR_PATH = System.getProperty("java.io.tmpdir"); 079 080 /** Unix file path separator */ 081 public static final char UNIX_SEPARATOR = '/'; 082 083 /** Windows file path separator */ 084 public static final char WINDOWS_SEPARATOR = '\\'; 085 086 /** 087 * String containing the unit letters for units in the metric system (K for 088 * kilo, M for mega, ...). Ordered by their power value (the order is 089 * important). 090 */ 091 public static final String METRIC_SYSTEM_UNITS = "KMGTPEZY"; 092 093 /** 094 * Pattern matching the start of the data-size unit in a data-size string (the 095 * first non-space char not belonging to the numeric value). 096 */ 097 private static final Pattern DATA_SIZE_UNIT_START_PATTERN = Pattern.compile("[^\\d\\s.,]"); 098 099 /** 100 * The names of path segments that are reserved in certain operating systems and 101 * hence may not be used. 102 * 103 * @see "https://answers.microsoft.com/en-us/windows/forum/windows_7-files/why-cant-i-create-a-folder-with-name/23c86662-4988-4c7d-9c2d-3e33d4413de3" 104 */ 105 private static final Set<String> RESERVED_PATH_SEGMENT_NAMES = new CaseInsensitiveStringSet( 106 Arrays.asList("CON", "PRN", "AUX", "CLOCK$", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", 107 "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9")); 108 109 /** 110 * Copy an input stream to an output stream. This does <em>not</em> close the 111 * streams. 112 * 113 * @param input 114 * input stream 115 * @param output 116 * output stream 117 * @return number of bytes copied 118 * @throws IOException 119 * if an IO exception occurs. 120 */ 121 public static int copy(InputStream input, OutputStream output) throws IOException { 122 byte[] buffer = new byte[1024]; 123 int size = 0; 124 int len; 125 while ((len = input.read(buffer)) > 0) { 126 output.write(buffer, 0, len); 127 size += len; 128 } 129 return size; 130 } 131 132 /** Copy a file. This creates all necessary directories. */ 133 @SuppressWarnings("resource") 134 public static void copyFile(File sourceFile, File targetFile) throws IOException { 135 136 if (sourceFile.getAbsoluteFile().equals(targetFile.getAbsoluteFile())) { 137 throw new IOException("Can not copy file onto itself: " + sourceFile); 138 } 139 140 ensureParentDirectoryExists(targetFile); 141 142 FileChannel sourceChannel = null; 143 FileChannel targetChannel = null; 144 try { 145 sourceChannel = new FileInputStream(sourceFile).getChannel(); 146 targetChannel = new FileOutputStream(targetFile).getChannel(); 147 sourceChannel.transferTo(0, sourceChannel.size(), targetChannel); 148 } finally { 149 close(sourceChannel); 150 close(targetChannel); 151 } 152 } 153 154 /** Copy a file. This creates all necessary directories. */ 155 public static void copyFile(String sourceFilename, String targetFilename) throws IOException { 156 copyFile(new File(sourceFilename), new File(targetFilename)); 157 } 158 159 /** 160 * Copy all files specified by a file filter from one directory to another. This 161 * automatically creates all necessary directories. 162 * 163 * @param fileFilter 164 * filter to specify file types. If all files should be copied, use 165 * {@link FileOnlyFilter}. 166 * @return number of files copied 167 */ 168 public static int copyFiles(File sourceDirectory, File targetDirectory, FileFilter fileFilter) throws IOException { 169 List<File> files = FileSystemUtils.listFilesRecursively(sourceDirectory, fileFilter); 170 171 int fileCount = 0; 172 for (File sourceFile : files) { 173 if (sourceFile.isFile()) { 174 String path = sourceFile.getAbsolutePath(); 175 int index = sourceDirectory.getAbsolutePath().length(); 176 String newPath = path.substring(index); 177 File targetFile = new File(targetDirectory, newPath); 178 copyFile(sourceFile, targetFile); 179 fileCount++; 180 } 181 } 182 return fileCount; 183 } 184 185 /** 186 * Create jar file from all files in a directory. 187 * 188 * @param filter 189 * filter to specify file types. If all files should be copied, use 190 * {@link FileOnlyFilter}. 191 * @return number of files added to the jar file 192 */ 193 public static int createJARFile(File jarFile, File sourceDirectory, FileFilter filter) throws IOException { 194 JarOutputStream out = null; 195 int fileCount = 0; 196 197 try { 198 out = new JarOutputStream(new FileOutputStream(jarFile)); 199 200 for (File file : FileSystemUtils.listFilesRecursively(sourceDirectory, filter)) { 201 if (!file.isFile()) { 202 continue; 203 } 204 205 FileInputStream in = null; 206 fileCount += 1; 207 try { 208 // works for forward slashes only 209 String entryName = normalizeSeparators( 210 file.getAbsolutePath().substring(sourceDirectory.getAbsolutePath().length() + 1)); 211 out.putNextEntry(new ZipEntry(entryName)); 212 213 in = new FileInputStream(file); 214 copy(in, out); 215 out.closeEntry(); 216 } finally { 217 close(in); 218 } 219 } 220 } finally { 221 close(out); 222 } 223 224 return fileCount; 225 } 226 227 /** 228 * Returns a string describing the relative path to the given directory. If 229 * there is no relative path, as the directories do not share a common parent, 230 * the absolute path is returned. 231 * 232 * @param path 233 * the path to convert to a relative path (must describe an existing 234 * directory) 235 * @param relativeTo 236 * the anchor (must describe an existing directory) 237 * @return a relative path 238 * @throws IOException 239 * if creation of canonical pathes fails. 240 */ 241 public static String createRelativePath(File path, File relativeTo) throws IOException { 242 CCSMAssert.isNotNull(path, "Path must not be null!"); 243 CCSMAssert.isNotNull(relativeTo, "relativeTo must not be null!"); 244 245 if (!path.isDirectory() || !relativeTo.isDirectory()) { 246 throw new IllegalArgumentException("Both arguments must be existing directories!"); 247 } 248 path = path.getCanonicalFile(); 249 relativeTo = relativeTo.getCanonicalFile(); 250 251 Set<File> parents = new HashSet<>(); 252 File f = path; 253 while (f != null) { 254 parents.add(f); 255 f = f.getParentFile(); 256 } 257 258 File root = relativeTo; 259 while (root != null && !parents.contains(root)) { 260 root = root.getParentFile(); 261 } 262 263 if (root == null) { 264 // no common root, so use full path 265 return path.getAbsolutePath(); 266 } 267 268 String result = ""; 269 while (!path.equals(root)) { 270 result = path.getName() + "/" + result; 271 path = path.getParentFile(); 272 } 273 while (!relativeTo.equals(root)) { 274 result = "../" + result; 275 relativeTo = relativeTo.getParentFile(); 276 } 277 278 return result; 279 } 280 281 /** 282 * Recursively delete directories and files. This method ignores the return 283 * value of delete(), i.e. if anything fails, some files might still exist. 284 */ 285 public static void deleteRecursively(File directory) { 286 287 if (directory == null) { 288 throw new IllegalArgumentException("Directory may not be null."); 289 } 290 File[] filesInDirectory = directory.listFiles(); 291 if (filesInDirectory == null) { 292 throw new IllegalArgumentException(directory.getAbsolutePath() + " is not a valid directory."); 293 } 294 295 for (File entry : filesInDirectory) { 296 if (entry.isDirectory()) { 297 deleteRecursively(entry); 298 } 299 entry.delete(); 300 } 301 directory.delete(); 302 } 303 304 /** 305 * Deletes the given file and throws an exception if this fails. 306 * 307 * @see File#delete() 308 */ 309 public static void deleteFile(File file) throws IOException { 310 if (file.exists() && !file.delete()) { 311 throw new IOException("Could not delete " + file); 312 } 313 } 314 315 /** 316 * Renames the given file and throws an exception if this fails. 317 * 318 * @see File#renameTo(File) 319 */ 320 public static void renameFileTo(File file, File dest) throws IOException { 321 if (!file.renameTo(dest)) { 322 throw new IOException("Could not rename " + file + " to " + dest); 323 } 324 } 325 326 /** 327 * Creates a directory and throws an exception if this fails. 328 * 329 * @see File#mkdir() 330 */ 331 public static void mkdir(File dir) throws IOException { 332 if (!dir.mkdir()) { 333 throw new IOException("Could not create directory " + dir); 334 } 335 } 336 337 /** 338 * Creates a directory and all required parent directories. Throws an exception 339 * if this fails. 340 * 341 * @see File#mkdirs() 342 */ 343 public static void mkdirs(File dir) throws IOException { 344 if (dir.exists() && dir.isDirectory()) { 345 // mkdirs will return false if the directory already exists, but in 346 // that case we don't want to throw an exception 347 return; 348 } 349 if (!dir.mkdirs()) { 350 throw new IOException("Could not create directory " + dir); 351 } 352 } 353 354 /** 355 * Checks if a directory exists. If not it creates the directory and all 356 * necessary parent directories. 357 * 358 * @throws IOException 359 * if directories couldn't be created. 360 */ 361 public static void ensureDirectoryExists(File directory) throws IOException { 362 if (!directory.exists() && !directory.mkdirs()) { 363 throw new IOException("Couldn't create directory: " + directory); 364 } 365 } 366 367 /** 368 * Checks if the parent directory of a file exists. If not it creates the 369 * directory and all necessary parent directories. 370 * 371 * @throws IOException 372 * if directories couldn't be created. 373 */ 374 public static void ensureParentDirectoryExists(File file) throws IOException { 375 ensureDirectoryExists(file.getCanonicalFile().getParentFile()); 376 } 377 378 /** 379 * Returns a list of all files and directories contained in the given directory 380 * and all subdirectories. The given directory itself is not included in the 381 * result. 382 * <p> 383 * This method knows nothing about (symbolic and hard) links, so care should be 384 * taken when traversing directories containing recursive links. 385 * 386 * @param directory 387 * the directory to start the search from. 388 * @return the list of files found (the order is determined by the file system). 389 */ 390 public static List<File> listFilesRecursively(File directory) { 391 return listFilesRecursively(directory, null); 392 } 393 394 /** 395 * Returns a list of all files and directories contained in the given directory 396 * and all subdirectories matching the filter provided. The given directory 397 * itself is not included in the result. 398 * <p> 399 * The file filter may or may not exclude directories. 400 * <p> 401 * This method knows nothing about (symbolic and hard) links, so care should be 402 * taken when traversing directories containing recursive links. 403 * 404 * @param directory 405 * the directory to start the search from. If this is null or the 406 * directory does not exist, an empty list is returned. 407 * @param filter 408 * the filter used to determine whether the result should be 409 * included. If the filter is null, all files and directories are 410 * included. 411 * @return the list of files found (the order is determined by the file system). 412 */ 413 public static List<File> listFilesRecursively(File directory, FileFilter filter) { 414 if (directory == null || !directory.isDirectory()) { 415 return CollectionUtils.emptyList(); 416 } 417 List<File> result = new ArrayList<>(); 418 listFilesRecursively(directory, result, filter); 419 return result; 420 } 421 422 /** 423 * @see #listFilesInSameLocationForURL(URL, boolean) 424 */ 425 public static List<String> listFilesInSameLocationForURL(URL baseUrl) throws IOException { 426 return listFilesInSameLocationForURL(baseUrl, false); 427 } 428 429 /** 430 * Lists the names of all simple files (i.e. no directories) next to an URL. For 431 * example for a file, this would return the names of all files in the same 432 * directory (including the file itself). Currently, this supports both file and 433 * jar URLs. The intended use-case is to list a set of files in a package via 434 * the class loader. 435 */ 436 public static List<String> listFilesInSameLocationForURL(URL baseUrl, boolean includeSubfolders) 437 throws IOException { 438 439 String protocol = baseUrl.getProtocol(); 440 441 if ("file".equals(protocol)) { 442 return listFilesForFileURL(baseUrl, includeSubfolders); 443 } 444 445 if ("jar".equals(protocol)) { 446 return listFilesForJarURL(baseUrl, includeSubfolders); 447 } 448 449 throw new IOException("Unsupported protocol: " + protocol); 450 } 451 452 /** 453 * Returns the parent path within the jar for a class file url. E.g. for the URL 454 * "jar:file:/path/to/file.jar!/sub/folder/File.class" the method returns 455 * "sub/folder/". If the url does already point to a directory it returns the 456 * path of this directory. 457 */ 458 private static String getJarUrlParentDirectoryPrefix(URL baseUrl) { 459 // in JAR URLs we can rely on the separator being a slash 460 String parentPath = StringUtils.getLastPart(baseUrl.getPath(), '!'); 461 parentPath = StringUtils.stripPrefix(parentPath, "/"); 462 if (parentPath.endsWith(".class")) { 463 parentPath = StringUtils.stripSuffix(parentPath, StringUtils.getLastPart(parentPath, UNIX_SEPARATOR)); 464 } else { 465 parentPath = StringUtils.ensureEndsWith(parentPath, String.valueOf(UNIX_SEPARATOR)); 466 } 467 return parentPath; 468 } 469 470 /** 471 * Lists the names of files for a JAR URL. If the recursive flag is set, all 472 * files within the same jar directory hierarchy will be listed, otherwise only 473 * the ones in the same directory. 474 */ 475 private static List<String> listFilesForJarURL(URL baseUrl, boolean recursive) throws IOException { 476 try (JarFile jarFile = new JarFile(FileSystemUtils.extractJarFileFromJarURL(baseUrl))) { 477 String parentPath = getJarUrlParentDirectoryPrefix(baseUrl); 478 return jarFile.stream().filter(entry -> shouldBeContainedInResult(entry, parentPath, recursive)) 479 .map(entry -> StringUtils.stripPrefix(entry.getName(), parentPath)).collect(Collectors.toList()); 480 } 481 } 482 483 /** 484 * Returns whether the jar entry should be returned when searching for files 485 * contained in the given path. 486 */ 487 private static boolean shouldBeContainedInResult(JarEntry entry, String path, boolean recursive) { 488 if (entry.isDirectory()) { 489 return false; 490 } 491 String simpleName = StringUtils.getLastPart(entry.getName(), UNIX_SEPARATOR); 492 String entryPath = StringUtils.stripSuffix(entry.getName(), simpleName); 493 494 return !recursive && entryPath.equals(path) || (recursive && entryPath.startsWith(path)); 495 } 496 497 /** 498 * Lists the names of files (not including directories) within the given file 499 * URL. This will only include subfolder (recursively) if the includeSubfolders 500 * flag is set. 501 * 502 * @return a list of relative, separator-normalized file paths without slash 503 * prefix, e.g. ['foo.java', 'subfolder/bar.java'] 504 */ 505 private static List<String> listFilesForFileURL(URL baseUrl, boolean includeSubfolders) throws IOException { 506 try { 507 File directory = new File(baseUrl.toURI()); 508 if (!directory.isDirectory()) { 509 directory = directory.getParentFile(); 510 } 511 if (!directory.isDirectory()) { 512 throw new IOException("Parent directory does not exist or is not readable for " + baseUrl); 513 } 514 515 if (includeSubfolders) { 516 File finalDirectory = directory; 517 return CollectionUtils.filterAndMap(listFilesRecursively(directory), File::isFile, file -> { 518 String relativeFilePath = StringUtils.stripPrefix(file.getAbsolutePath(), 519 finalDirectory.getAbsolutePath()); 520 relativeFilePath = FileSystemUtils.normalizeSeparators(relativeFilePath); 521 return StringUtils.stripPrefix(relativeFilePath, String.valueOf(UNIX_SEPARATOR)); 522 }); 523 } 524 525 List<String> names = new ArrayList<>(); 526 for (File file : directory.listFiles()) { 527 if (file.isFile()) { 528 names.add(file.getName()); 529 } 530 } 531 return names; 532 } catch (URISyntaxException e) { 533 throw new IOException("Could not convert URL to valid file: " + baseUrl, e); 534 } 535 } 536 537 /** 538 * Extract all top-level classes in the given JAR and returns a list of their 539 * fully qualified class names. Inner classes are ignored. 540 */ 541 public static List<String> listTopLevelClassesInJarFile(File jarFile) throws IOException { 542 List<String> result = new ArrayList<>(); 543 PathBasedContentProviderBase provider = PathBasedContentProviderBase.createProvider(jarFile); 544 Collection<String> paths = provider.getPaths(); 545 for (String path : paths) { 546 if (path.endsWith(ClassPathUtils.CLASS_FILE_SUFFIX) && !path.contains("$")) { 547 String fqn = StringUtils.removeLastPart(path, '.'); 548 fqn = fqn.replace(UNIX_SEPARATOR, '.'); 549 result.add(fqn); 550 } 551 } 552 return result; 553 } 554 555 /** 556 * Returns the extension of the file. 557 * 558 * @return File extension, i.e. "java" for "FileSystemUtils.java", or 559 * <code>null</code>, if the file has no extension (i.e. if a filename 560 * contains no '.'), returns the empty string if the '.' is the 561 * filename's last character. 562 */ 563 public static String getFileExtension(File file) { 564 return getFileExtension(file.getName()); 565 } 566 567 /** Returns the extension of the file at the given path. */ 568 public static String getFileExtension(String path) { 569 int posLastDot = path.lastIndexOf('.'); 570 if (posLastDot < 0) { 571 return null; 572 } 573 return path.substring(posLastDot + 1); 574 } 575 576 /** 577 * Returns the name of the given file without extension. Example: 578 * '/home/joe/data.dat' -> 'data'. 579 */ 580 public static String getFilenameWithoutExtension(File file) { 581 return getFilenameWithoutExtension(file.getName()); 582 } 583 584 /** 585 * Returns the name of the given file without extension. Example: 'data.dat' -> 586 * 'data'. 587 */ 588 public static String getFilenameWithoutExtension(String fileName) { 589 return StringUtils.removeLastPart(fileName, '.'); 590 } 591 592 /** 593 * Returns the last path segment (i.e. file name or folder name) of a file path. 594 */ 595 public static String getLastPathSegment(String filePath) { 596 String[] split = getPathSegments(filePath); 597 return split[split.length - 1]; 598 } 599 600 /** Returns the segments of a path. */ 601 public static String[] getPathSegments(String filePath) { 602 return FileSystemUtils.normalizeSeparators(filePath).split(String.valueOf(UNIX_SEPARATOR)); 603 } 604 605 /** 606 * Constructs a file from a base file by appending several path elements. 607 * Insertion of separators is performed automatically as needed. This is similar 608 * to the constructor {@link File#File(File, String)} but allows to define 609 * multiple child levels. 610 * 611 * @param pathElements 612 * list of elements. If this is empty, the parent is returned. 613 * @return the new file. 614 */ 615 public static File newFile(File parentFile, String... pathElements) { 616 if (pathElements.length == 0) { 617 return parentFile; 618 } 619 620 File child = new File(parentFile, pathElements[0]); 621 622 String[] remainingElements = new String[pathElements.length - 1]; 623 System.arraycopy(pathElements, 1, remainingElements, 0, pathElements.length - 1); 624 return newFile(child, remainingElements); 625 } 626 627 /** 628 * Read file content into a string using the default encoding for the platform. 629 * If the file starts with a UTF byte order mark (BOM), the encoding is ignored 630 * and the correct encoding based on this BOM is used for reading the file. 631 * 632 * @see EByteOrderMark 633 */ 634 public static String readFile(File file) throws IOException { 635 return readFile(file, Charset.defaultCharset()); 636 } 637 638 /** 639 * Read file content into a string using UTF-8 encoding. If the file starts with 640 * a UTF byte order mark (BOM), the encoding is ignored and the correct encoding 641 * based on this BOM is used for reading the file. 642 * 643 * @see EByteOrderMark 644 */ 645 public static String readFileUTF8(File file) throws IOException { 646 return readFile(file, StandardCharsets.UTF_8); 647 } 648 649 /** 650 * Read file content into a string using UTF-8 encoding and normalize the line 651 * breaks. If the file starts with a UTF byte order mark (BOM), the encoding is 652 * ignored and the correct encoding based on this BOM is used for reading the 653 * file. 654 * 655 * @see EByteOrderMark 656 * @see StringUtils#normalizeLineSeparatorsPlatformSpecific(String) 657 */ 658 public static String readFileUTF8WithLineBreakNormalization(File file) throws IOException { 659 return StringUtils.normalizeLineSeparatorsPlatformSpecific(readFile(file, StandardCharsets.UTF_8)); 660 } 661 662 /** 663 * Read file content into a string using the given encoding. If the file starts 664 * with a UTF byte order mark (BOM), the encoding is ignored and the correct 665 * encoding based on this BOM is used for reading the file. 666 * 667 * @see EByteOrderMark 668 */ 669 public static String readFile(File file, Charset encoding) throws IOException { 670 byte[] buffer = readFileBinary(file); 671 return StringUtils.bytesToString(buffer, encoding); 672 } 673 674 /** 675 * Read file content into a list of lines (strings) using the given encoding. 676 * This uses automatic BOM handling, just as {@link #readFile(File)}. 677 */ 678 public static List<String> readLines(File file, Charset encoding) throws IOException { 679 return StringUtils.splitLinesAsList(readFile(file, encoding)); 680 } 681 682 /** 683 * Read file content into a list of lines (strings) using UTF-8 encoding. This 684 * uses automatic BOM handling, just as {@link #readFile(File)}. 685 */ 686 public static List<String> readLinesUTF8(String filePath) throws IOException { 687 return readLinesUTF8(new File(filePath)); 688 } 689 690 /** 691 * Read file content into a list of lines (strings) using UTF-8 encoding. This 692 * uses automatic BOM handling, just as {@link #readFile(File)}. 693 */ 694 public static List<String> readLinesUTF8(File file) throws IOException { 695 return readLines(file, StandardCharsets.UTF_8); 696 } 697 698 /** Read file content into a byte array. */ 699 public static byte[] readFileBinary(String filePath) throws IOException { 700 return readFileBinary(new File(filePath)); 701 } 702 703 /** Read file content into a byte array. */ 704 public static byte[] readFileBinary(File file) throws IOException { 705 FileInputStream in = new FileInputStream(file); 706 707 byte[] buffer = new byte[(int) file.length()]; 708 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 709 710 FileChannel channel = in.getChannel(); 711 try { 712 int readSum = 0; 713 while (readSum < buffer.length) { 714 int read = channel.read(byteBuffer); 715 if (read < 0) { 716 throw new IOException("Reached EOF before entire file could be read!"); 717 } 718 readSum += read; 719 } 720 } finally { 721 close(channel); 722 close(in); 723 } 724 725 return buffer; 726 } 727 728 /** Extract a JAR file to a directory. */ 729 public static void unjar(File jarFile, File targetDirectory) throws IOException { 730 unzip(jarFile, targetDirectory); 731 } 732 733 /** 734 * Extract a ZIP file to a directory. 735 */ 736 public static void unzip(File zipFile, File targetDirectory) throws IOException { 737 unzip(zipFile, targetDirectory, null, null); 738 } 739 740 /** 741 * Extract the entries of ZIP file to a directory. 742 * 743 * @param prefix 744 * Sets a prefix for the the entry names (paths) which should be 745 * extracted. Only entries which starts with the given prefix are 746 * extracted. If prefix is <code>null</code> or empty all entries are 747 * extracted. The prefix will be stripped form the extracted entries. 748 * @param charset 749 * defines the {@link Charset} of the ZIP file. If <code>null</code>, 750 * the standard of {@link ZipFile} is used (which is UTF-8). 751 * @return list of the extracted paths 752 */ 753 public static List<String> unzip(File zipFile, File targetDirectory, String prefix, Charset charset) 754 throws IOException { 755 ZipFile zip = null; 756 try { 757 if (charset == null) { 758 zip = new ZipFile(zipFile); 759 } else { 760 zip = new ZipFile(zipFile, charset); 761 } 762 return unzip(zip, targetDirectory, prefix); 763 } finally { 764 close(zip); 765 } 766 } 767 768 /** 769 * Extract entries of a ZipFile to a directory when the ZipFile is created 770 * externally. Note that this does not close the ZipFile, so the caller has to 771 * take care of this. 772 */ 773 public static List<String> unzip(ZipFile zip, File targetDirectory, String prefix) throws IOException { 774 Enumeration<? extends ZipArchiveEntry> entries = zip.getEntries(); 775 List<String> extractedPaths = new ArrayList<>(); 776 777 while (entries.hasMoreElements()) { 778 ZipArchiveEntry entry = entries.nextElement(); 779 if (entry.isDirectory()) { 780 continue; 781 } 782 String fileName = entry.getName(); 783 if (!StringUtils.isEmpty(prefix)) { 784 if (!fileName.startsWith(prefix)) { 785 continue; 786 } 787 fileName = StringUtils.stripPrefix(fileName, prefix); 788 } 789 790 try (InputStream entryStream = zip.getInputStream(entry)) { 791 File file = new File(targetDirectory, fileName); 792 ensureParentDirectoryExists(file); 793 794 try (FileOutputStream outputStream = new FileOutputStream(file)) { 795 copy(entryStream, outputStream); 796 } 797 } 798 extractedPaths.add(fileName); 799 } 800 return extractedPaths; 801 } 802 803 /** 804 * Extract entries of a zip file input stream to a directory. The input stream 805 * is automatically closed by this method. 806 */ 807 public static List<String> unzip(InputStream inputStream, File targetDirectory) throws IOException { 808 List<String> extractedPaths = new ArrayList<>(); 809 try (ZipInputStream zipStream = new ZipInputStream(inputStream)) { 810 while (true) { 811 ZipEntry entry = zipStream.getNextEntry(); 812 if (entry == null) { 813 break; 814 } else if (entry.isDirectory()) { 815 continue; 816 } 817 String fileName = entry.getName(); 818 819 File file = new File(targetDirectory, fileName); 820 ensureParentDirectoryExists(file); 821 822 try (OutputStream targetStream = new FileOutputStream(file)) { 823 copy(zipStream, targetStream); 824 } 825 extractedPaths.add(fileName); 826 } 827 } 828 return extractedPaths; 829 } 830 831 /** 832 * Write string to a file with the default encoding. This ensures all 833 * directories exist. 834 */ 835 public static void writeFile(File file, String content) throws IOException { 836 writeFile(file, content, Charset.defaultCharset().name()); 837 } 838 839 /** 840 * Writes the given collection of String as lines into the specified file. This 841 * method uses \n as a line separator. 842 */ 843 public static void writeLines(File file, Collection<String> lines) throws IOException { 844 writeFile(file, StringUtils.concat(lines, "\n")); 845 } 846 847 /** 848 * Write string to a file with UTF8 encoding. This ensures all directories 849 * exist. 850 */ 851 public static void writeFileUTF8(File file, String content) throws IOException { 852 writeFile(file, content, UTF8_ENCODING); 853 } 854 855 /** Write string to a file. This ensures all directories exist. */ 856 public static void writeFile(File file, String content, String encoding) throws IOException { 857 ensureParentDirectoryExists(file); 858 OutputStreamWriter writer = null; 859 try { 860 writer = new OutputStreamWriter(new FileOutputStream(file), encoding); 861 writer.write(content); 862 } finally { 863 FileSystemUtils.close(writer); 864 } 865 } 866 867 /** 868 * Write string to a file using a UTF encoding. The file will be prefixed with a 869 * byte-order mark. This ensures all directories exist. 870 */ 871 public static void writeFileWithBOM(File file, String content, EByteOrderMark bom) throws IOException { 872 ensureParentDirectoryExists(file); 873 FileOutputStream out = null; 874 OutputStreamWriter writer = null; 875 try { 876 out = new FileOutputStream(file); 877 out.write(bom.getBOM()); 878 879 writer = new OutputStreamWriter(out, bom.getEncoding()); 880 writer.write(content); 881 writer.flush(); 882 } finally { 883 FileSystemUtils.close(out); 884 FileSystemUtils.close(writer); 885 } 886 } 887 888 /** 889 * Writes the given bytes to the given file. Directories are created as needed. 890 * The file is closed after writing. 891 */ 892 public static void writeFileBinary(File file, byte[] bytes) throws IOException { 893 ensureParentDirectoryExists(file); 894 FileOutputStream out = null; 895 try { 896 out = new FileOutputStream(file); 897 out.write(bytes); 898 } finally { 899 FileSystemUtils.close(out); 900 } 901 } 902 903 /** 904 * Finds all files and directories contained in the given directory and all 905 * subdirectories matching the filter provided and put them into the result 906 * collection. The given directory itself is not included in the result. 907 * <p> 908 * This method knows nothing about (symbolic and hard) links, so care should be 909 * taken when traversing directories containing recursive links. 910 * 911 * @param directory 912 * the directory to start the search from. 913 * @param result 914 * the collection to add to all files found. 915 * @param filter 916 * the filter used to determine whether the result should be 917 * included. If the filter is null, all files and directories are 918 * included. 919 */ 920 private static void listFilesRecursively(File directory, Collection<File> result, FileFilter filter) { 921 for (File file : directory.listFiles()) { 922 if (file.isDirectory()) { 923 listFilesRecursively(file, result, filter); 924 } 925 if (filter == null || filter.accept(file)) { 926 result.add(file); 927 } 928 } 929 } 930 931 /** 932 * Loads template file with a <a href= 933 * "http://java.sun.com/javase/6/docs/api/java/util/Formatter.html#syntax" > 934 * Format string</a>, formats it and writes result to file. 935 * 936 * @param templateFile 937 * the template file with the format string 938 * @param outFile 939 * the target file, parent directories are created automatically. 940 * @param arguments 941 * the formatting arguments. 942 * @throws IOException 943 * if an IO exception occurs or the template file defines an illegal 944 * format. 945 */ 946 public static void mergeTemplate(File templateFile, File outFile, Object... arguments) throws IOException { 947 String template = readFile(templateFile); 948 String output; 949 try { 950 output = String.format(template, arguments); 951 } catch (IllegalFormatException e) { 952 throw new IOException("Illegal format: " + e.getMessage(), e); 953 } 954 writeFile(outFile, output); 955 } 956 957 /** 958 * Loads template file with a <a href= 959 * "http://java.sun.com/javase/6/docs/api/java/util/Formatter.html#syntax" > 960 * Format string</a>, formats it and provides result as stream. No streams are 961 * closed by this method. 962 * 963 * @param inStream 964 * stream that provides the template format string 965 * @param arguments 966 * the formatting arguments. 967 * @throws IOException 968 * if an IOException occurs or the template file defines an illegal 969 * format. 970 */ 971 public static InputStream mergeTemplate(InputStream inStream, Object... arguments) throws IOException { 972 String template = readStream(inStream); 973 String output; 974 try { 975 output = String.format(template, arguments); 976 } catch (IllegalFormatException e) { 977 throw new IOException("Illegal format: " + e.getMessage(), e); 978 } 979 return new ByteArrayInputStream(output.getBytes()); 980 } 981 982 /** Read input stream into string. */ 983 public static String readStream(InputStream input) throws IOException { 984 return readStream(input, Charset.defaultCharset()); 985 } 986 987 /** Read input stream into string. */ 988 public static String readStreamUTF8(InputStream input) throws IOException { 989 return readStream(input, StandardCharsets.UTF_8); 990 } 991 992 /** 993 * Read input stream into string. This method is BOM aware, i.e. deals with the 994 * UTF-BOM. 995 */ 996 public static String readStream(InputStream input, Charset encoding) throws IOException { 997 StringBuilder out = new StringBuilder(); 998 Reader r = streamReader(input, encoding); 999 char[] b = new char[4096]; 1000 1001 int n; 1002 while ((n = r.read(b)) != -1) { 1003 out.append(b, 0, n); 1004 } 1005 return out.toString(); 1006 } 1007 1008 /** Read input stream into raw byte array. */ 1009 public static byte[] readStreamBinary(InputStream input) throws IOException { 1010 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1011 copy(input, out); 1012 return out.toByteArray(); 1013 } 1014 1015 /** 1016 * Returns a reader that wraps the given input stream. This method handles the 1017 * BOM transparently. As the normal reader constructors can not deal with this, 1018 * direct construction of readers is discouraged. 1019 */ 1020 public static Reader streamReader(InputStream in, Charset encoding) throws IOException { 1021 // we need marking to read the BOM mark 1022 if (!in.markSupported()) { 1023 in = new BufferedInputStream(in); 1024 } 1025 1026 in.mark(EByteOrderMark.MAX_BOM_LENGTH); 1027 byte[] prefix = new byte[EByteOrderMark.MAX_BOM_LENGTH]; 1028 1029 EByteOrderMark bom = null; 1030 try { 1031 safeRead(in, prefix); 1032 bom = EByteOrderMark.determineBOM(prefix).orElse(null); 1033 } catch (IOException e) { 1034 // just use provided encoding; keep BOM as null 1035 } 1036 1037 in.reset(); 1038 1039 if (bom != null) { 1040 encoding = bom.getEncoding(); 1041 1042 // consume BOM 1043 for (int i = 0; i < bom.getBOMLength(); ++i) { 1044 in.read(); 1045 } 1046 } 1047 1048 return new InputStreamReader(in, encoding); 1049 } 1050 1051 /** Reads properties from a properties file. */ 1052 public static Properties readProperties(File propertiesFile) throws IOException { 1053 return readProperties(() -> new FileInputStream(propertiesFile)); 1054 } 1055 1056 /** Reads properties from a properties resource. */ 1057 public static Properties readProperties(Resource resource) throws IOException { 1058 return readProperties(resource::getAsStream); 1059 } 1060 1061 /** Reads properties from a properties stream. */ 1062 public static Properties readProperties( 1063 CollectionUtils.SupplierWithException<? extends InputStream, IOException> streamSupplier) 1064 throws IOException { 1065 try (InputStream stream = streamSupplier.get()) { 1066 Properties props = new Properties(); 1067 props.load(stream); 1068 return props; 1069 } 1070 } 1071 1072 /** 1073 * Determines the root directory from a collection of files. The root directory 1074 * is the lowest common ancestor directory of the files in the directory tree. 1075 * <p> 1076 * This method does not require the input files to exist. 1077 * 1078 * @param files 1079 * Collection of files for which root directory gets determined. This 1080 * collection is required to contain at least 2 files. If it does 1081 * not, an AssertionError is thrown. 1082 * 1083 * @throws AssertionError 1084 * If less than two different files are provided whereas fully 1085 * qualified canonical names are used for comparison. 1086 * 1087 * @throws IOException 1088 * Since canonical paths are used for determination of the common 1089 * root, and {@link File#getCanonicalPath()} can throw 1090 * {@link IOException}s. 1091 * 1092 * @return Root directory, or null, if the files do not have a common root 1093 * directory. 1094 */ 1095 public static File commonRoot(Iterable<? extends File> files) throws IOException { 1096 // determine longest common prefix on canonical absolute paths 1097 Set<String> absolutePaths = new HashSet<>(); 1098 for (File file : files) { 1099 absolutePaths.add(file.getCanonicalPath()); 1100 } 1101 1102 CCSMAssert.isTrue(absolutePaths.size() >= 2, "Expected are at least 2 files"); 1103 1104 String longestCommonPrefix = StringUtils.longestCommonPrefix(absolutePaths); 1105 1106 // trim to name of root directory (remove possible equal filename 1107 // prefixes.) 1108 int lastSeparator = longestCommonPrefix.lastIndexOf(File.separator); 1109 if (lastSeparator > -1) { 1110 longestCommonPrefix = longestCommonPrefix.substring(0, lastSeparator); 1111 } 1112 1113 if (StringUtils.isEmpty(longestCommonPrefix)) { 1114 return null; 1115 } 1116 1117 return new File(longestCommonPrefix); 1118 } 1119 1120 /** 1121 * Transparently creates a stream for decompression if the provided stream is 1122 * compressed. Otherwise the stream is just handed through. Currently the 1123 * following compression methods are supported: 1124 * <ul> 1125 * <li>GZIP via {@link GZIPInputStream}</li> 1126 * </ul> 1127 */ 1128 public static InputStream autoDecompressStream(InputStream in) throws IOException { 1129 if (!in.markSupported()) { 1130 in = new BufferedInputStream(in); 1131 } 1132 in.mark(2); 1133 // check first two bytes for GZIP header 1134 boolean isGZIP = (in.read() & 0xff | (in.read() & 0xff) << 8) == GZIPInputStream.GZIP_MAGIC; 1135 in.reset(); 1136 if (isGZIP) { 1137 return new GZIPInputStream(in); 1138 } 1139 return in; 1140 } 1141 1142 /** 1143 * Closes the given ZIP file quietly, i.e. ignoring a potential IOException. 1144 * Additionally it is <code>null</code> safe. 1145 */ 1146 public static void close(ZipFile zipFile) { 1147 if (zipFile == null) { 1148 return; 1149 } 1150 try { 1151 zipFile.close(); 1152 } catch (IOException e) { 1153 // ignore 1154 } 1155 } 1156 1157 /** 1158 * Convenience method for calling {@link #close(Closeable, ILogger)} with a 1159 * <code>null</code>-logger. 1160 */ 1161 public static void close(Closeable closeable) { 1162 close(closeable, null); 1163 } 1164 1165 /** 1166 * This method can be used to simplify the typical <code>finally</code> -block 1167 * of code dealing with streams and readers/writers. It checks if the provided 1168 * closeable is <code>null</code>. If not it closes it. An exception thrown 1169 * during the close operation is logged with the provided logger with level 1170 * <i>warn</i>. If the provided logger is <code>null</code>, no logging is 1171 * performed. If no logging is required, method {@link #close(Closeable)} may 1172 * also be used. 1173 */ 1174 public static void close(Closeable closeable, ILogger logger) { 1175 if (closeable == null) { 1176 return; 1177 } 1178 1179 try { 1180 closeable.close(); 1181 } catch (IOException e) { 1182 if (logger != null) { 1183 logger.warn("Trouble closing: " + e.getMessage()); 1184 } 1185 } 1186 } 1187 1188 /** 1189 * Compares files based on the lexical order of their fully qualified names. 1190 * Files must not null. 1191 */ 1192 public static void sort(List<File> files) { 1193 files.sort(new FilenameComparator()); 1194 } 1195 1196 /** 1197 * Replace platform dependent separator char with forward slashes to create 1198 * system-independent paths. 1199 */ 1200 public static String normalizeSeparators(String path) { 1201 return path.replace(File.separatorChar, UNIX_SEPARATOR); 1202 } 1203 1204 /** 1205 * Returns the JAR file for an URL with protocol 'jar'. If the protocol is not 1206 * 'jar' an assertion error will be caused! An assertion error is also thrown if 1207 * URL does not point to a file. 1208 */ 1209 public static File extractJarFileFromJarURL(URL url) { 1210 CCSMAssert.isTrue("jar".equals(url.getProtocol()), "May only be used with 'jar' URLs!"); 1211 1212 String path = url.getPath(); 1213 CCSMAssert.isTrue(path.startsWith("file:"), "May only be used for URLs pointing to files"); 1214 1215 // the exclamation mark is the separator between jar file and path 1216 // within the file 1217 int index = path.indexOf('!'); 1218 CCSMAssert.isTrue(index >= 0, "Unknown format for jar URLs"); 1219 path = path.substring(0, index); 1220 1221 return fromURL(path); 1222 } 1223 1224 /** 1225 * Often file URLs are created the wrong way, i.e. without proper escaping 1226 * characters invalid in URLs. Unfortunately, the URL class allows this and the 1227 * Eclipse framework does it. See 1228 * http://weblogs.java.net/blog/2007/04/25/how-convert-javaneturl-javaiofile for 1229 * details. 1230 * 1231 * This method attempts to fix this problem and create a file from it. 1232 * 1233 * @throws AssertionError 1234 * if cleaning up fails. 1235 */ 1236 private static File fromURL(String url) { 1237 1238 // We cannot simply encode the URL this also encodes slashes and other 1239 // stuff. As a result, the file constructor throws an exception. As a 1240 // simple heuristic, we only fix the spaces. 1241 // The other route to go would be manually stripping of "file:" and 1242 // simply creating a file. However, this does not work if the URL was 1243 // created properly and contains URL escapes. 1244 1245 url = url.replace(StringUtils.SPACE, "%20"); 1246 try { 1247 return new File(new URI(url)); 1248 } catch (URISyntaxException e) { 1249 throw new AssertionError("The assumption is that this method is capable of " 1250 + "working with non-standard-compliant URLs, too. " + "Apparently it is not. Invalid URL: " + url 1251 + ". Ex: " + e.getMessage(), e); 1252 } 1253 } 1254 1255 /** 1256 * Returns whether a filename represents an absolute path. 1257 * 1258 * This method returns the same result, independent on which operating system it 1259 * gets executed. In contrast, the behavior of {@link File#isAbsolute()} is 1260 * operating system specific. 1261 */ 1262 public static boolean isAbsolutePath(String filename) { 1263 // Unix and MacOS: absolute path starts with slash or user home 1264 if (filename.startsWith("/") || filename.startsWith("~")) { 1265 return true; 1266 } 1267 // Windows and OS/2: absolute path start with letter and colon 1268 if (filename.length() > 2 && Character.isLetter(filename.charAt(0)) && filename.charAt(1) == ':') { 1269 return true; 1270 } 1271 // UNC paths (aka network shares): start with double backslash 1272 if (filename.startsWith("\\\\")) { 1273 return true; 1274 } 1275 1276 return false; 1277 } 1278 1279 /** 1280 * Reads bytes of data from the input stream into an array of bytes until the 1281 * array is full. This method blocks until input data is available, end of file 1282 * is detected, or an exception is thrown. 1283 * 1284 * The reason for this method is that {@link InputStream#read(byte[])} may read 1285 * less than the requested number of bytes, while this method ensures the data 1286 * is complete. 1287 * 1288 * @param in 1289 * the stream to read from. 1290 * @param data 1291 * the stream to read from. 1292 * @throws IOException 1293 * if reading the underlying stream causes an exception. 1294 * @throws EOFException 1295 * if the end of file was reached before the requested data was 1296 * read. 1297 */ 1298 public static void safeRead(InputStream in, byte[] data) throws IOException, EOFException { 1299 safeRead(in, data, 0, data.length); 1300 } 1301 1302 /** 1303 * Reads <code>length</code> bytes of data from the input stream into an array 1304 * of bytes and stores it at position <code>offset</code>. This method blocks 1305 * until input data is available, end of file is detected, or an exception is 1306 * thrown. 1307 * 1308 * The reason for this method is that {@link InputStream#read(byte[], int, int)} 1309 * may read less than the requested number of bytes, while this method ensures 1310 * the data is complete. 1311 * 1312 * @param in 1313 * the stream to read from. 1314 * @param data 1315 * the stream to read from. 1316 * @param offset 1317 * the offset in the array where the first read byte is stored. 1318 * @param length 1319 * the length of data read. 1320 * @throws IOException 1321 * if reading the underlying stream causes an exception. 1322 * @throws EOFException 1323 * if the end of file was reached before the requested data was 1324 * read. 1325 */ 1326 public static void safeRead(InputStream in, byte[] data, int offset, int length) throws IOException, EOFException { 1327 while (length > 0) { 1328 int read = in.read(data, offset, length); 1329 if (read < 0) { 1330 throw new EOFException("Reached end of file before completing read."); 1331 } 1332 offset += read; 1333 length -= read; 1334 } 1335 } 1336 1337 /** Obtains the system's temporary directory */ 1338 public static File getTmpDir() { 1339 return new File(TEMP_DIR_PATH); 1340 } 1341 1342 /** Obtains the current user's home directory */ 1343 public static File getUserHomeDir() { 1344 return new File(System.getProperty("user.home")); 1345 } 1346 1347 /** 1348 * Obtains the current working directory. This is usually the directory in which 1349 * the current Java process was started. 1350 */ 1351 public static File getWorkspaceDir() { 1352 if (Boolean.getBoolean("com.teamscale.dev-mode") || isJUnitTest()) { 1353 return getTmpDir(); 1354 } 1355 return new File(System.getProperty("user.dir")); 1356 } 1357 1358 private static boolean isJUnitTest() { 1359 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 1360 for (StackTraceElement element : stackTrace) { 1361 if (element.getClassName().startsWith("org.junit.")) { 1362 return true; 1363 } 1364 } 1365 return false; 1366 } 1367 1368 /** Checks whether two files have the same content. */ 1369 public static boolean contentsEqual(File file1, File file2) throws IOException { 1370 byte[] content1 = readFileBinary(file1); 1371 byte[] content2 = readFileBinary(file2); 1372 return Arrays.equals(content1, content2); 1373 } 1374 1375 /** 1376 * Opens an {@link InputStream} for the entry with the given name in the given 1377 * JAR file 1378 */ 1379 public static InputStream openJarFileEntry(JarFile jarFile, String entryName) throws IOException { 1380 JarEntry entry = jarFile.getJarEntry(entryName); 1381 if (entry == null) { 1382 throw new IOException("No entry '" + entryName + "' found in JAR file '" + jarFile + "'"); 1383 } 1384 return jarFile.getInputStream(entry); 1385 } 1386 1387 /** 1388 * Returns whether the given file is non-null, a plain file and is readable. 1389 */ 1390 public static boolean isReadableFile(File... files) { 1391 for (File file : files) { 1392 if (file == null || !file.exists() || !file.isFile() || !file.canRead()) { 1393 return false; 1394 } 1395 } 1396 return true; 1397 } 1398 1399 /** 1400 * Concatenates all path parts into a single path with normalized separators. 1401 */ 1402 public static String concatenatePaths(String firstParent, String... paths) { 1403 return FileSystemUtils.normalizeSeparators(Paths.get(firstParent, paths).toString()); 1404 } 1405 1406 /** 1407 * Removes the given path if it is an empty directory and recursively parent 1408 * directories if the only child was deleted. 1409 * 1410 * If the given path points to file which does not exist, the parent directory 1411 * of that file is deleted. 1412 * 1413 * @throws IOException 1414 * if an I/O error during deletion occurs. 1415 */ 1416 public static void recursivelyRemoveDirectoryIfEmpty(File path) throws IOException { 1417 String[] children = path.list(); 1418 if (children == null) { 1419 // path either points to a plain file or to a non-existent 1420 // path. In the first case, nothing should be done otherwise 1421 // deletion should continue with the parent path. 1422 if (path.exists()) { 1423 return; 1424 } 1425 } else if (children.length == 0) { 1426 deleteFile(path); 1427 } else { 1428 return; 1429 } 1430 recursivelyRemoveDirectoryIfEmpty(path.getParentFile()); 1431 } 1432 1433 /** 1434 * Returns a file that does not exist in the same directory of the given file. 1435 * It either returns the passed file (if not exiting) or appends a number 1436 * between the filename and the extension to ensure uniqueness, e.g. 1437 * path/to/file_1.ext if path/to/file.ext is passed but this file already 1438 * exists. The number is incremented until the file does not exist. 1439 */ 1440 public static File getNonExistingFile(File file) { 1441 if (!file.exists()) { 1442 return file; 1443 } 1444 1445 String extensionlessName = getFilenameWithoutExtension(file); 1446 int suffix = 0; 1447 String extension = FileSystemUtils.getFileExtension(file); 1448 1449 do { 1450 file = new File(file.getParentFile(), extensionlessName + "_" + ++suffix + "." + extension); 1451 } while (file.exists()); 1452 1453 return file; 1454 } 1455 1456 /** 1457 * Converts the given human readable data size to the corresponding number of 1458 * bytes. For example "1 KB" is converted to 1024. Also supports Si units ("1 1459 * KiB" is converted to 1000). 1460 * 1461 * Commas are ignored and can be used as thousands separator. A dot is the 1462 * decimal separator. ("1.2KiB" is converted to 1200). 1463 * 1464 * Method implementation based on stackoverflow 1465 * https://stackoverflow.com/a/45860167 1466 */ 1467 public static long parseDataSize(String dataSize) { 1468 String dataSizeWithoutComma = dataSize.replaceAll(",", ""); 1469 int unitBeginIndex = StringUtils.indexOfMatch(dataSizeWithoutComma, DATA_SIZE_UNIT_START_PATTERN); 1470 if (unitBeginIndex == -1) { 1471 return Long.parseLong(dataSizeWithoutComma); 1472 } 1473 double rawDataSize = Double.parseDouble(dataSizeWithoutComma.substring(0, unitBeginIndex)); 1474 String unitString = dataSizeWithoutComma.substring(unitBeginIndex); 1475 int unitChar = unitString.charAt(0); 1476 int power = METRIC_SYSTEM_UNITS.indexOf(unitChar) + 1; 1477 boolean isSi = unitBeginIndex != -1 && unitString.length() >= 2 && unitString.charAt(1) == 'i'; 1478 1479 int factor = 1024; 1480 if (isSi) { 1481 factor = 1000; 1482 if (StringUtils.stripSuffix(unitString, "B").length() != 2) { 1483 throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma); 1484 } 1485 } else if (power == 0) { 1486 if (StringUtils.stripSuffix(unitString, "B").length() != 0) { 1487 throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma); 1488 } 1489 } else { 1490 if (StringUtils.stripSuffix(unitString, "B").length() != 1) { 1491 throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma); 1492 } 1493 } 1494 return (long) (rawDataSize * Math.pow(factor, power)); 1495 } 1496 1497 /** Determines the last modified timestamp in a platform agnostic way. */ 1498 public static long getLastModifiedTimestamp(File file) throws IOException { 1499 return Files.getLastModifiedTime(Paths.get(file.toURI())).toMillis(); 1500 } 1501 1502 /** 1503 * Returns a safe filename that can be used for downloads. Replaces everything 1504 * that is not a letter or number with "-" 1505 */ 1506 public static String toSafeFilename(String name) { 1507 name = name.replaceAll("\\W+", "-"); 1508 name = name.replaceAll("[-_]+", "-"); 1509 return name; 1510 } 1511 1512 /** 1513 * Returns a new file with all file path segments that are reserved (windows) 1514 * path names escaped. 1515 */ 1516 public static File escapeReservedFileNames(File file) { 1517 String[] parts = file.getPath().split(Pattern.quote(File.separator)); 1518 for (int i = 0; i < parts.length; ++i) { 1519 if (RESERVED_PATH_SEGMENT_NAMES.contains(parts[i])) { 1520 parts[i] = "_" + parts[i]; 1521 } 1522 } 1523 return new File(StringUtils.concat(parts, File.separator)); 1524 } 1525 1526 /** 1527 * Reads a file using UTF-8 encoding and normalizes line breaks (replacing 1528 * "\n\r" and "\r" with "\n"). This generates an OS-independent view on a file. 1529 * To ensure OS-independent test results, this method should be used in all 1530 * tests to read files. 1531 */ 1532 public static String readFileSystemIndependent(File file) throws IOException { 1533 return StringUtils.normalizeLineSeparatorsPlatformIndependent(readFileUTF8(file)); 1534 1535 } 1536 1537 /** 1538 * Replaces the file name of the given path with the given new extension. 1539 * Returns the newFileName if the file denoted by the uniform path does not 1540 * contain a '/'. This method assumes that folders are separated by '/' (uniform 1541 * paths). 1542 * 1543 * Examples: 1544 * <ul> 1545 * <li><code>replaceFilePathFilenameWith("xx", "yy")</code> returns 1546 * <code>"yy"</code></li> 1547 * <li><code>replaceFilePathFilenameWith("xx/zz", "yy")</code> returns * 1548 * <code>"xx/yy"</code></li> 1549 * <li><code>replaceFilePathFilenameWith("xx/zz/", "yy")</code> returns * 1550 * <code>"xx/zz/yy"</code></li> 1551 * <li><code>replaceFilePathFilenameWith("", "yy")</code> returns * 1552 * <code>"yy"</code></li> 1553 * </ul> 1554 */ 1555 public static String replaceFilePathFilenameWith(String uniformPath, String newFileName) { 1556 int folderSepIndex = uniformPath.lastIndexOf('/'); 1557 1558 if (uniformPath.endsWith("/")) { 1559 return uniformPath + newFileName; 1560 } else if (folderSepIndex == -1) { 1561 return newFileName; 1562 } 1563 return uniformPath.substring(0, folderSepIndex) + "/" + newFileName; 1564 } 1565}