package org.jabref.logic.cleanup; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; 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.stream.Collectors; import org.jabref.logic.layout.LayoutFormatterPreferences; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.FieldChange; import org.jabref.model.cleanup.CleanupJob; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.metadata.FileDirectoryPreferences; import org.jabref.model.util.FileHelper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class RenamePdfCleanup implements CleanupJob { private static final Log LOGGER = LogFactory.getLog(RenamePdfCleanup.class); private final BibDatabaseContext databaseContext; private final boolean onlyRelativePaths; private final String fileNamePattern; private final LayoutFormatterPreferences layoutPrefs; private final FileDirectoryPreferences fileDirectoryPreferences; private int unsuccessfulRenames; private LinkedFile singleFieldCleanup; public RenamePdfCleanup(boolean onlyRelativePaths, BibDatabaseContext databaseContext, String fileNamePattern, LayoutFormatterPreferences layoutPrefs, FileDirectoryPreferences fileDirectoryPreferences) { this.databaseContext = Objects.requireNonNull(databaseContext); this.onlyRelativePaths = onlyRelativePaths; this.fileNamePattern = Objects.requireNonNull(fileNamePattern); this.layoutPrefs = Objects.requireNonNull(layoutPrefs); this.fileDirectoryPreferences = fileDirectoryPreferences; } public RenamePdfCleanup(boolean onlyRelativePaths, BibDatabaseContext databaseContext, String fileNamePattern, LayoutFormatterPreferences layoutPrefs, FileDirectoryPreferences fileDirectoryPreferences, LinkedFile singleField) { this(onlyRelativePaths, databaseContext, fileNamePattern, layoutPrefs, fileDirectoryPreferences); this.singleFieldCleanup = singleField; } @Override public List<FieldChange> cleanup(BibEntry entry) { List<LinkedFile> newFileList; List<LinkedFile> fileList; if (singleFieldCleanup != null) { fileList = Arrays.asList(singleFieldCleanup); newFileList = entry.getFiles().stream().filter(x -> !x.equals(singleFieldCleanup)) .collect(Collectors.toList()); } else { newFileList = new ArrayList<>(); fileList = entry.getFiles(); } boolean changed = false; for (LinkedFile flEntry : fileList) { String realOldFilename = flEntry.getLink(); if (onlyRelativePaths && Paths.get(realOldFilename).isAbsolute()) { newFileList.add(flEntry); continue; } //old path and old filename Optional<Path> expandedOldFile = flEntry.findIn(databaseContext, fileDirectoryPreferences); if ((!expandedOldFile.isPresent()) || (expandedOldFile.get().getParent() == null)) { // something went wrong. Just skip this entry newFileList.add(flEntry); continue; } String targetFileName = getTargetFileName(flEntry, entry); Path newPath = expandedOldFile.get().getParent().resolve(targetFileName); String expandedOldFilePath = expandedOldFile.get().toString(); boolean pathsDifferOnlyByCase = newPath.toString().equalsIgnoreCase(expandedOldFilePath) && !newPath.toString().equals(expandedOldFilePath); if (Files.exists(newPath) && !pathsDifferOnlyByCase) { // we do not overwrite files // Since File.exists is sometimes not case-sensitive, the check pathsDifferOnlyByCase ensures that we // nonetheless rename files to a new name which just differs by case. // TODO: we could check here if the newPath file is linked with the current entry. And if not, we could add a link LOGGER.debug("There already exists a file with that name " + newPath.getFileName() + " so I won't rename it"); newFileList.add(flEntry); continue; } try { if (!Files.exists(newPath)) { Files.createDirectories(newPath); } } catch (IOException e) { LOGGER.error("Could not create necessary target directoires for renaming", e); } boolean renameSuccessful = FileUtil.renameFile(Paths.get(expandedOldFilePath), newPath, true); if (renameSuccessful) { changed = true; //Change the path for this entry String description = flEntry.getDescription(); String type = flEntry.getFileType(); //We use the file directory (if none is set - then bib file) to create relative file links Optional<Path> settingsDir = databaseContext.getFirstExistingFileDir(fileDirectoryPreferences); if (settingsDir.isPresent()) { Path parent = settingsDir.get(); String newFileEntryFileName; if (parent == null) { newFileEntryFileName = targetFileName; } else { newFileEntryFileName = parent.relativize(newPath).toString(); } newFileList.add(new LinkedFile(description, newFileEntryFileName, type)); } } else { unsuccessfulRenames++; } } if (changed) { Optional<FieldChange> change = entry.setFiles(newFileList); //we put an undo of the field content here //the file is not being renamed back, which leads to inconsistencies //if we put a null undo object here, the change by "doMakePathsRelative" would overwrite the field value nevertheless. if (change.isPresent()) { return Collections.singletonList(change.get()); } else { return Collections.emptyList(); } } return Collections.emptyList(); } public String getTargetFileName(LinkedFile flEntry, BibEntry entry) { String realOldFilename = flEntry.getLink(); StringBuilder targetFileName = new StringBuilder(FileUtil .createFileNameFromPattern(databaseContext.getDatabase(), entry, fileNamePattern, layoutPrefs) .trim()); //Add extension to newFilename targetFileName.append('.').append(FileHelper.getFileExtension(realOldFilename).orElse("pdf")); return targetFileName.toString(); } public int getUnsuccessfulRenames() { return unsuccessfulRenames; } }