package org.jabref.logic.util.io; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Stack; import java.util.Vector; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jabref.logic.layout.Layout; import org.jabref.logic.layout.LayoutFormatterPreferences; import org.jabref.logic.layout.LayoutHelper; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.util.OptionalUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class FileUtil { public static final boolean isPosixCompilant = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); private static final Log LOGGER = LogFactory.getLog(FileUtil.class); private FileUtil() { } /** * Returns the name part of a file name (i.e., everything in front of last "."). */ public static String getFileName(String fileNameWithExtension) { int dotPosition = fileNameWithExtension.lastIndexOf('.'); if (dotPosition >= 0) { return fileNameWithExtension.substring(0, dotPosition); } else { return fileNameWithExtension; } } /** * Adds an extension to the given file name. The original extension is not replaced. That means, * "demo.bib", ".sav" gets "demo.bib.sav" and not "demo.sav" * * @param path the path to add the extension to * @param extension the extension to add * @return the with the modified file name */ public static Path addExtension(Path path, String extension) { Path fileName = path.getFileName(); return path.resolveSibling(fileName + extension); } /** * Creates the minimal unique path substring for each file among multiple file paths. * * @param paths the file paths * @return the minimal unique path substring for each file path */ public static List<String> uniquePathSubstrings(List<String> paths) { List<Stack<String>> stackList = new ArrayList<>(paths.size()); // prepare data structures for (String path : paths) { List<String> directories = Arrays.asList(path.split(Pattern.quote(File.separator))); Stack<String> stack = new Stack<>(); stack.addAll(directories); stackList.add(stack); } List<String> pathSubstrings = new ArrayList<>(Collections.nCopies(paths.size(), "")); // compute shortest folder substrings while (!stackList.stream().allMatch(Vector::isEmpty)) { for (int i = 0; i < stackList.size(); i++) { String tempString = pathSubstrings.get(i); if (tempString.isEmpty() && !stackList.get(i).isEmpty()) { pathSubstrings.set(i, stackList.get(i).pop()); } else if (!stackList.get(i).isEmpty()) { pathSubstrings.set(i, stackList.get(i).pop() + File.separator + tempString); } } for (int i = 0; i < stackList.size(); i++) { String tempString = pathSubstrings.get(i); if (Collections.frequency(pathSubstrings, tempString) == 1) { stackList.get(i).clear(); } } } return pathSubstrings; } /** * Copies a file. * * @param pathToSourceFile Path Source file * @param pathToDestinationFile Path Destination file * @param replaceExisting boolean Determines whether the copy goes on even if the file exists. * @return boolean Whether the copy succeeded, or was stopped due to the file already existing. * @throws IOException */ public static boolean copyFile(Path pathToSourceFile, Path pathToDestinationFile, boolean replaceExisting) { // Check if the file already exists. if (!Files.exists(pathToSourceFile)) { LOGGER.error("Path to the source file doesn't exist."); return false; } if (Files.exists(pathToDestinationFile) && !replaceExisting) { LOGGER.error("Path to the destination file is not exists and the file shouldn't be replace."); return false; } try { return Files.copy(pathToSourceFile, pathToDestinationFile, StandardCopyOption.REPLACE_EXISTING) != null; } catch (IOException e) { LOGGER.error("Copying Files failed.", e); return false; } } /** * Renames a given file * * @param fromFile The source filename to rename * @param toFile The target fileName * @return True if the rename was successful, false if an exception occurred */ public static boolean renameFile(Path fromFile, Path toFile) { return renameFile(fromFile, toFile, false); } /** * Renames a given file * * @param fromFile The source filename to rename * @param toFile The target fileName * @param replaceExisting Wether to replace existing files or not * @return True if the rename was successful, false if an exception occurred * */ public static boolean renameFile(Path fromFile, Path toFile, boolean replaceExisting) { try { if (replaceExisting) { return Files.move(fromFile, fromFile.resolveSibling(toFile), StandardCopyOption.REPLACE_EXISTING) != null; } else { return Files.move(fromFile, fromFile.resolveSibling(toFile)) != null; } } catch (IOException e) { LOGGER.error("Renaming Files failed", e); return false; } } /** * Converts an absolute file to a relative one, if possible. * Returns the parameter file itself if no shortening is possible * <p> * This method works correctly only if dirs are sorted decent in their length * i.e. /home/user/literature/important before /home/user/literature * * @param file the file to be shortened * @param dirs directories to check */ public static Path shortenFileName(Path file, List<Path> dirs) { if (!file.isAbsolute()) { return file; } for (Path dir : dirs) { if (file.startsWith(dir)) { return dir.relativize(file); } } return file; } /** * Returns the list of linked files. The files have the absolute filename * * @param bes list of BibTeX entries * @param fileDirs list of directories to try for expansion * * @return list of files. May be empty */ public static List<Path> getListOfLinkedFiles(List<BibEntry> bes, List<Path> fileDirs) { Objects.requireNonNull(bes); Objects.requireNonNull(fileDirs); return bes.stream() .flatMap(entry -> entry.getFiles().stream()) .flatMap(file -> OptionalUtil.toStream(file.findIn(fileDirs))) .collect(Collectors.toList()); } /** * Determines filename provided by an entry in a database * * @param database the database, where the entry is located * @param entry the entry to which the file should be linked to * @param fileNamePattern the filename pattern * @param prefs the layout preferences * @return a suggested fileName */ public static String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern, LayoutFormatterPreferences prefs) { String targetName = null; StringReader sr = new StringReader(fileNamePattern); Layout layout = null; try { layout = new LayoutHelper(sr, prefs).getLayoutFromText(); } catch (IOException e) { LOGGER.info("Wrong format " + e.getMessage(), e); } if (layout != null) { targetName = layout.doLayout(entry, database); } if ((targetName == null) || targetName.isEmpty()) { targetName = entry.getCiteKeyOptional().orElse("default"); } //Removes illegal characters from filename targetName = FileNameCleaner.cleanFileName(targetName); return targetName; } /** * Finds a file inside a directory structure. * Will also look for the file inside nested directories. * * @param filename the name of the file that should be found * @param rootDirectory the rootDirectory that will be searched * @return the path to the first file that matches the defined conditions */ public static Optional<Path> find(String filename, Path rootDirectory) { try { return Files.walk(rootDirectory) .filter(Files::isRegularFile) .filter(f -> f.getFileName().toString().equals(filename)) .findFirst(); } catch (IOException ex) { LOGGER.error("Error trying to locate the file " + filename + " inside the directory " + rootDirectory); } return Optional.empty(); } /** * Finds a file inside a list of directory structures. * Will also look for the file inside nested directories. * * @param filename the name of the file that should be found * @param directories the directories that will be searched * @return a list including all found paths to files that match the defined conditions */ public static List<Path> find(String filename, List<Path> directories) { List<Path> files = new ArrayList<>(); for (Path dir : directories) { FileUtil.find(filename, dir).ifPresent(files::add); } return files; } }