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}