/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2008 Alex Buloichik 2009 Didier Briel 2012 Alex Buloichik, Didier Briel 2014 Alex Buloichik, Aaron Madlon-Kay 2015-2016 Aaron Madlon-Kay Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OmegaT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; /** * Files processing utilities. * * @author Alex Buloichik (alex73mail@gmail.com) * @author Didier Briel * @author Aaron Madlon-Kay */ public final class FileUtil { public static final long RENAME_RETRY_TIMEOUT = 3000; private FileUtil() { } /** * Removes old backups so that only 10 last are there. */ public static void removeOldBackups(final File originalFile, int maxBackups) { try { File[] bakFiles = originalFile.getParentFile().listFiles(new FileFilter() { public boolean accept(File f) { return !f.isDirectory() && f.getName().startsWith(originalFile.getName()) && f.getName().endsWith(OConsts.BACKUP_EXTENSION); } }); if (bakFiles != null && bakFiles.length > maxBackups) { Arrays.sort(bakFiles, new Comparator<File>() { public int compare(File f1, File f2) { if (f2.lastModified() < f1.lastModified()) { return -1; } else if (f2.lastModified() > f1.lastModified()) { return 1; } else { return 0; } } }); for (int i = maxBackups; i < bakFiles.length; i++) { bakFiles[i].delete(); } } } catch (Exception e) { // we don't care } } /** * Create file backup with datetime suffix. */ public static void backupFile(File f) throws IOException { long fileMillis = f.lastModified(); String str = new SimpleDateFormat("yyyyMMddHHmm").format(new Date(fileMillis)); FileUtils.copyFile(f, new File(f.getPath() + "." + str + OConsts.BACKUP_EXTENSION)); } /** * Renames file, with checking errors and 3 seconds retry against external programs (like antivirus or * TortoiseSVN) locking. */ public static void rename(File from, File to) throws IOException { if (!from.exists()) { throw new IOException("Source file to rename (" + from + ") doesn't exist"); } if (to.exists()) { throw new IOException("Target file to rename (" + to + ") already exists"); } long b = System.currentTimeMillis(); while (!from.renameTo(to)) { long e = System.currentTimeMillis(); if (e - b > RENAME_RETRY_TIMEOUT) { throw new IOException("Error renaming " + from + " to " + to); } } } /** * Copy file and create output directory if need. EOL will be converted into target-specific or into * platform-specific if target doesn't exist. */ public static void copyFileWithEolConversion(File inFile, File outFile, Charset charset) throws IOException { File dir = outFile.getParentFile(); if (!dir.exists()) { dir.mkdirs(); } String eol; if (outFile.exists()) { // file exist - read EOL from file eol = getEOL(outFile, charset); } else { // file not exist - use system-dependent eol = System.lineSeparator(); } try (BufferedReader in = Files.newBufferedReader(inFile.toPath(), charset)) { try (BufferedWriter out = Files.newBufferedWriter(outFile.toPath(), charset)) { String s; while ((s = in.readLine()) != null) { // copy using known EOL out.write(s); out.write(eol); } } } } public static String getEOL(File file, Charset charset) throws IOException { String r = null; try (BufferedReader in = Files.newBufferedReader(file.toPath(), charset)) { while (true) { int ch = in.read(); if (ch < 0) { break; } if (ch == '\n' || ch == '\r') { r = Character.toString((char) ch); int ch2 = in.read(); if (ch2 == '\n' || ch2 == '\r') { r += Character.toString((char) ch2); } break; } } } return r; } /** * Find files in subdirectories. * * @param dir * directory to start find * @param filter * filter for found files * @return list of filtered found files */ public static List<File> findFiles(final File dir, final FileFilter filter) { final List<File> result = new ArrayList<File>(); Set<String> knownDirs = new HashSet<String>(); findFiles(dir, filter, result, knownDirs); return result; } /** * Internal find method, which calls himself recursively. * * @param dir * directory to start find * @param filter * filter for found files * @param result * list of filtered found files */ private static void findFiles(final File dir, final FileFilter filter, final List<File> result, final Set<String> knownDirs) { String currDir; try { // check for recursive currDir = dir.getCanonicalPath(); if (!knownDirs.add(currDir)) { return; } } catch (IOException ex) { Log.log(ex); return; } File[] list = dir.listFiles(); if (list != null) { for (File f : list) { if (f.isDirectory()) { findFiles(f, filter, result, knownDirs); } else { if (filter.accept(f)) { result.add(f); } } } } } /** * Compute relative path of file. * * @param rootDir * root directory * @param filePath * file path * @return */ public static String computeRelativePath(File rootDir, File file) throws IOException { String rootAbs = rootDir.getAbsolutePath().replace('\\', '/') + '/'; String fileAbs = file.getAbsolutePath().replace('\\', '/'); switch (Platform.getOsType()) { case WIN32: case WIN64: if (!fileAbs.toUpperCase().startsWith(rootAbs.toUpperCase())) { throw new IOException("File '" + file + "' is not under dir '" + rootDir + "'"); } break; default: if (!fileAbs.startsWith(rootAbs)) { throw new IOException("File '" + file + "' is not under dir '" + rootDir + "'"); } break; } return fileAbs.substring(rootAbs.length()); } /** * Check if file is in specified path. */ public static boolean isInPath(File path, File tmxFile) { try { computeRelativePath(path, tmxFile); return true; } catch (IOException ex) { return false; } } public interface ICollisionCallback { boolean isCanceled(); boolean shouldReplace(File file, int thisFile, int totalFiles); } /** * Copy a collection of files to a destination. Recursively copies contents of directories * while preserving relative paths. Provide an {@link ICollisionCallback} to determine * what to do with files with conflicting names; they will be overwritten if the callback is null. * @param destination Directory to copy to * @param toCopy Files to copy * @param onCollision Callback that determines what to do in case files with the same name * already exist * @throws IOException */ public static void copyFilesTo(File destination, File[] toCopy, ICollisionCallback onCollision) throws IOException { if (destination.exists() && !destination.isDirectory()) { throw new IOException("Copy-to destination exists and is not a directory."); } Map<File, File> collisions = copyFilesTo(destination, toCopy, (File) null); if (collisions.isEmpty()) { return; } List<File> toReplace = new ArrayList<File>(); List<File> toDelete = new ArrayList<File>(); int count = 0; for (Entry<File, File> e : collisions.entrySet()) { if (onCollision != null && onCollision.isCanceled()) { break; } if (onCollision == null || onCollision.shouldReplace(e.getValue(), count, collisions.size())) { toReplace.add(e.getKey()); toDelete.add(e.getValue()); } count++; } if (onCollision == null || !onCollision.isCanceled()) { for (File file : toDelete) { FileUtils.forceDelete(file); } copyFilesTo(destination, toReplace.toArray(new File[toReplace.size()]), (File) null); } } private static Map<File, File> copyFilesTo(File destination, File[] toCopy, File root) throws IOException { Map<File, File> collisions = new LinkedHashMap<File, File>(); for (File file : toCopy) { if (destination.getPath().startsWith(file.getPath())) { // Trying to copy something into its own subtree continue; } File thisRoot = root == null ? file.getParentFile() : root; String filePath = file.getPath(); String relPath = filePath.substring(thisRoot.getPath().length(), filePath.length()); File dest = new File(destination, relPath); if (file.equals(dest)) { // Trying to copy file to itself. Skip. continue; } if (dest.exists()) { collisions.put(file, dest); continue; } if (file.isDirectory()) { copyFilesTo(destination, file.listFiles(), thisRoot); } else { FileUtils.copyFile(file, dest); } } return collisions; } private static final Pattern RE_ABSOLUTE_WINDOWS = Pattern.compile("[A-Za-z]\\:(/.*)"); private static final Pattern RE_ABSOLUTE_LINUX = Pattern.compile("/.*"); /** * Checks if path starts with possible root on the Linux, MacOS, Windows. */ public static boolean isRelative(String path) { path = path.replace('\\', '/'); return !RE_ABSOLUTE_LINUX.matcher(path).matches() && !RE_ABSOLUTE_WINDOWS.matcher(path).matches(); } /** * Converts Windows absolute path into current system's absolute path. It required for conversion like * 'C:\zzz' into '/zzz' for be real absolute in Linux. */ public static String absoluteForSystem(String path, Platform.OsType currentOsType) { path = path.replace('\\', '/'); Matcher m = RE_ABSOLUTE_WINDOWS.matcher(path); if (m.matches()) { if (currentOsType != Platform.OsType.WIN32 && currentOsType != Platform.OsType.WIN64) { // Windows' absolute file on non-Windows system return m.group(1); } } return path; } /** * Returns a list of all files under the root directory by absolute path. * * @throws IOException */ public static List<File> buildFileList(File rootDir, boolean recursive) throws IOException { int depth = recursive ? Integer.MAX_VALUE : 0; try (Stream<Path> stream = Files.find(rootDir.toPath(), depth, (p, attr) -> p.toFile().isFile(), FileVisitOption.FOLLOW_LINKS)) { return stream.map(Path::toFile).sorted(StreamUtil.localeComparator(File::getPath)) .collect(Collectors.toList()); } } public static List<String> buildRelativeFilesList(File rootDir, List<String> includes, List<String> excludes) throws IOException { Path root = rootDir.toPath(); Pattern[] includeMasks = FileUtil.compileFileMasks(includes); Pattern[] excludeMasks = FileUtil.compileFileMasks(excludes); BiPredicate<Path, BasicFileAttributes> pred = (p, attr) -> { return p.toFile().isFile() && FileUtil.checkFileInclude(root.relativize(p).toString(), includeMasks, excludeMasks); }; try (Stream<Path> stream = Files.find(root, Integer.MAX_VALUE, pred, FileVisitOption.FOLLOW_LINKS)) { return stream.map(p -> root.relativize(p).toString().replace('\\', '/')) .sorted(StreamUtil.localeComparator(Function.identity())) .collect(Collectors.toList()); } } public static boolean checkFileInclude(String filePath, Pattern[] includes, Pattern[] excludes) { String normalized = filePath.replace('\\', '/'); String checkPath = normalized.startsWith("/") ? normalized : '/' + normalized; boolean included = Stream.of(includes).map(p -> p.matcher(checkPath)).anyMatch(Matcher::matches); boolean excluded = false; if (!included) { excluded = Stream.of(excludes).map(p -> p.matcher(checkPath)).anyMatch(Matcher::matches); } return included || !excluded; } static Pattern[] compileFileMasks(List<String> masks) { if (masks == null) { return FileUtil.NO_PATTERNS; } return masks.stream().map(FileUtil::compileFileMask).toArray(Pattern[]::new); } private static final Pattern[] NO_PATTERNS = new Pattern[0]; static Pattern compileFileMask(String mask) { StringBuilder m = new StringBuilder(); // "Relative" masks can match at any directory level if (!mask.startsWith("/")) { mask = "**/" + mask; } // Masks ending with a slash match everything in subtree if (mask.endsWith("/")) { mask += "**"; } for (int cp, i = 0; i < mask.length(); i += Character.charCount(cp)) { cp = mask.codePointAt(i); if (cp >= 'A' && cp <= 'Z') { m.appendCodePoint(cp); } else if (cp >= 'a' && cp <= 'z') { m.appendCodePoint(cp); } else if (cp >= '0' && cp <= '9') { m.appendCodePoint(cp); } else if (cp == '/') { if (mask.regionMatches(i, "/**/", 0, 4)) { // The sequence /**/ matches *zero* or more levels m.append("(?:/|/.*/)"); i += 3; } else if (mask.regionMatches(i, "/**", 0, 3)) { // The sequence /** matches *zero* or more levels m.append("(?:|/.*)"); i += 2; } else { m.appendCodePoint(cp); } } else if (cp == '?') { // ? matches anything but a directory separator m.append("[^/]"); } else if (cp == '*') { if (mask.regionMatches(i, "**/", 0, 3)) { // The sequence **/ matches *zero* or more levels m.append("(?:|.*/)"); i += 2; } else if (mask.regionMatches(i, "**", 0, 2)) { // ** m.append(".*"); i++; } else { // * m.append("[^/]*"); } } else { m.append('\\').appendCodePoint(cp); } } return Pattern.compile(m.toString()); } }