diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cfda86aba0..41a596a3abb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added ability to export in CFF (Citation File Format) [#10661](https://github.com/JabRef/jabref/issues/10661).
- We added ability to push entries to TeXworks. [#3197](https://github.com/JabRef/jabref/issues/3197)
- We added the ability to zoom in and out in the document viewer using Ctrl + Scroll. [#10964](https://github.com/JabRef/jabref/pull/10964)
+- We added a Cleanup for removing non-existent files and grouped the related options [#10929](https://github.com/JabRef/jabref/issues/10929)
### Changed
diff --git a/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.fxml b/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.fxml
index 2ef2300a312..0812f829c71 100644
--- a/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.fxml
+++ b/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.fxml
@@ -14,26 +14,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.java b/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.java
index 1a0cf3d6df2..18dd701e626 100644
--- a/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.java
+++ b/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.java
@@ -33,6 +33,7 @@ public class CleanupPresetPanel extends VBox {
@FXML private CheckBox cleanUpMakePathsRelative;
@FXML private CheckBox cleanUpRenamePDF;
@FXML private CheckBox cleanUpRenamePDFonlyRelativePaths;
+ @FXML private CheckBox cleanUpDeletedFiles;
@FXML private CheckBox cleanUpUpgradeExternalLinks;
@FXML private CheckBox cleanUpBiblatex;
@FXML private CheckBox cleanUpBibtex;
@@ -71,6 +72,7 @@ private void init(CleanupPreferences cleanupPreferences, FilePreferences filePre
.concat(": ")
.concat(filePreferences.getFileNamePattern());
cleanupRenamePDFLabel.setText(currentPattern);
+
cleanUpBibtex.selectedProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue) {
@@ -109,6 +111,7 @@ private void updateDisplay(CleanupPreferences preset) {
cleanUpRenamePDF.setSelected(preset.isActive(CleanupPreferences.CleanupStep.RENAME_PDF));
cleanUpRenamePDFonlyRelativePaths.setSelected(preset.isActive(CleanupPreferences.CleanupStep.RENAME_PDF_ONLY_RELATIVE_PATHS));
cleanUpUpgradeExternalLinks.setSelected(preset.isActive(CleanupPreferences.CleanupStep.CLEAN_UP_UPGRADE_EXTERNAL_LINKS));
+ cleanUpDeletedFiles.setSelected(preset.isActive(CleanupPreferences.CleanupStep.CLEAN_UP_DELETED_LINKED_FILES));
cleanUpBiblatex.setSelected(preset.isActive(CleanupPreferences.CleanupStep.CONVERT_TO_BIBLATEX));
cleanUpBibtex.setSelected(preset.isActive(CleanupPreferences.CleanupStep.CONVERT_TO_BIBTEX));
cleanUpTimestampToCreationDate.setSelected(preset.isActive(CleanupPreferences.CleanupStep.CONVERT_TIMESTAMP_TO_CREATIONDATE));
@@ -150,6 +153,9 @@ public CleanupPreferences getCleanupPreset() {
if (cleanUpUpgradeExternalLinks.isSelected()) {
activeJobs.add(CleanupPreferences.CleanupStep.CLEAN_UP_UPGRADE_EXTERNAL_LINKS);
}
+ if (cleanUpDeletedFiles.isSelected()) {
+ activeJobs.add(CleanupPreferences.CleanupStep.CLEAN_UP_DELETED_LINKED_FILES);
+ }
if (cleanUpBiblatex.isSelected()) {
activeJobs.add(CleanupPreferences.CleanupStep.CONVERT_TO_BIBLATEX);
}
diff --git a/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java b/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
index 1663db75f87..d6e642f9b7b 100644
--- a/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
+++ b/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
@@ -67,6 +67,8 @@ private CleanupJob toJob(CleanupPreferences.CleanupStep action) {
new RenamePdfCleanup(true, databaseContext, filePreferences);
case CLEAN_UP_UPGRADE_EXTERNAL_LINKS ->
new UpgradePdfPsToFileCleanup();
+ case CLEAN_UP_DELETED_LINKED_FILES ->
+ new RemoveLinksToNotExistentFiles(databaseContext, filePreferences);
case CONVERT_TO_BIBLATEX ->
new ConvertToBiblatexCleanup();
case CONVERT_TO_BIBTEX ->
diff --git a/src/main/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFiles.java b/src/main/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFiles.java
new file mode 100644
index 00000000000..1d2c009aa95
--- /dev/null
+++ b/src/main/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFiles.java
@@ -0,0 +1,52 @@
+package org.jabref.logic.cleanup;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.jabref.model.FieldChange;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.LinkedFile;
+import org.jabref.model.util.OptionalUtil;
+import org.jabref.preferences.FilePreferences;
+
+public class RemoveLinksToNotExistentFiles implements CleanupJob {
+ private final BibDatabaseContext databaseContext;
+ private final FilePreferences filePreferences;
+
+ public RemoveLinksToNotExistentFiles(BibDatabaseContext databaseContext, FilePreferences filePreferences) {
+ this.databaseContext = Objects.requireNonNull(databaseContext);
+ this.filePreferences = Objects.requireNonNull(filePreferences);
+ }
+
+ @Override
+ public List cleanup(BibEntry entry) {
+ List files = entry.getFiles();
+ List cleanedUpFiles = new ArrayList<>();
+ boolean changed = false;
+ for (LinkedFile file : files) {
+ if (file.isOnlineLink()) {
+ cleanedUpFiles.add(file);
+ } else {
+ Optional oldFile = file.findIn(databaseContext, filePreferences);
+
+ if (oldFile.isEmpty()) {
+ changed = true;
+ } else {
+ cleanedUpFiles.add(file);
+ }
+ }
+ }
+
+ if (changed) {
+ Optional changes = entry.setFiles(cleanedUpFiles);
+ return OptionalUtil.toList(changes);
+ }
+
+ return Collections.emptyList();
+ }
+}
diff --git a/src/main/java/org/jabref/preferences/CleanupPreferences.java b/src/main/java/org/jabref/preferences/CleanupPreferences.java
index cf3b24491fa..dec2183eba6 100644
--- a/src/main/java/org/jabref/preferences/CleanupPreferences.java
+++ b/src/main/java/org/jabref/preferences/CleanupPreferences.java
@@ -88,6 +88,7 @@ public enum CleanupStep {
* Collects file links from the pdf or ps field, and adds them to the list contained in the file field.
*/
CLEAN_UP_UPGRADE_EXTERNAL_LINKS,
+ CLEAN_UP_DELETED_LINKED_FILES,
/**
* Converts to biblatex format
*/
diff --git a/src/test/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFilesTest.java b/src/test/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFilesTest.java
new file mode 100644
index 00000000000..cd02a2534cd
--- /dev/null
+++ b/src/test/java/org/jabref/logic/cleanup/RemoveLinksToNotExistentFilesTest.java
@@ -0,0 +1,163 @@
+package org.jabref.logic.cleanup;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.jabref.logic.bibtex.FileFieldWriter;
+import org.jabref.model.FieldChange;
+import org.jabref.model.database.BibDatabase;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.LinkedFile;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.types.StandardEntryType;
+import org.jabref.model.metadata.MetaData;
+import org.jabref.preferences.FilePreferences;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RemoveLinksToNotExistentFilesTest {
+ private Path fileBefore;
+ private BibEntry entry;
+ private RemoveLinksToNotExistentFiles removeLinks;
+
+ @BeforeEach
+ void setUp(@TempDir Path bibFolder) throws IOException {
+ // The folder where the files should be moved to
+ Path newFileFolder = bibFolder.resolve("pdf");
+ Files.createDirectory(newFileFolder);
+
+ Path originalFileFolder = bibFolder.resolve("files");
+ Path testBibFolder = bibFolder.resolve("test.bib");
+ Files.createDirectory(originalFileFolder);
+ fileBefore = originalFileFolder.resolve("test.pdf");
+ Files.createFile(fileBefore);
+
+ MetaData metaData = new MetaData();
+ metaData.setDefaultFileDirectory(newFileFolder.toAbsolutePath().toString());
+
+ BibDatabaseContext databaseContext = new BibDatabaseContext(new BibDatabase(), metaData);
+ Files.createFile(testBibFolder);
+ databaseContext.setDatabasePath(testBibFolder);
+
+ LinkedFile fileField = new LinkedFile("", fileBefore.toAbsolutePath(), "");
+
+ // Entry with one online and one normal linked file
+ entry = new BibEntry(StandardEntryType.Article)
+ .withField(StandardField.AUTHOR, "Shatakshi Sharma and Bhim Singh and Sukumar Mishra")
+ .withField(StandardField.DATE, "April 2020")
+ .withField(StandardField.YEAR, "2020")
+ .withField(StandardField.DOI, "10.1109/TII.2019.2935531")
+ .withField(StandardField.FILE, FileFieldWriter.getStringRepresentation(List.of(
+ new LinkedFile("", "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8801912", "PDF"),
+ fileField)))
+ .withField(StandardField.ISSUE, "4")
+ .withField(StandardField.ISSN, "1941-0050")
+ .withField(StandardField.JOURNALTITLE, "IEEE Transactions on Industrial Informatics")
+ .withField(StandardField.PAGES, "2346--2356")
+ .withField(StandardField.PUBLISHER, "IEEE")
+ .withField(StandardField.TITLE, "Economic Operation and Quality Control in PV-BES-DG-Based Autonomous System")
+ .withField(StandardField.VOLUME, "16")
+ .withField(StandardField.KEYWORDS, "Batteries, Generators, Economics, Power quality, State of charge, Harmonic analysis, Control systems, Battery, diesel generator (DG), distributed generation, power quality, photovoltaic (PV), voltage source converter (VSC)");
+
+ FilePreferences filePreferences = mock(FilePreferences.class);
+ when(filePreferences.shouldStoreFilesRelativeToBibFile()).thenReturn(false);
+ removeLinks = new RemoveLinksToNotExistentFiles(databaseContext, filePreferences);
+ }
+
+ @Test
+ void deleteFileInEntryWithMultipleFileLinks() throws IOException {
+ LinkedFile fileField = new LinkedFile("", fileBefore.toAbsolutePath(), "");
+ FieldChange expectedChange = new FieldChange(entry, StandardField.FILE,
+ FileFieldWriter.getStringRepresentation(List.of(
+ new LinkedFile("", "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8801912", "PDF"),
+ fileField)),
+ FileFieldWriter.getStringRepresentation(new LinkedFile("", "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8801912", "PDF"))
+ );
+ BibEntry expectedEntry = new BibEntry(StandardEntryType.Article)
+ .withField(StandardField.AUTHOR, "Shatakshi Sharma and Bhim Singh and Sukumar Mishra")
+ .withField(StandardField.DATE, "April 2020")
+ .withField(StandardField.YEAR, "2020")
+ .withField(StandardField.DOI, "10.1109/TII.2019.2935531")
+ .withField(StandardField.FILE, FileFieldWriter.getStringRepresentation(
+ new LinkedFile("", "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8801912", "PDF")))
+ .withField(StandardField.ISSUE, "4")
+ .withField(StandardField.ISSN, "1941-0050")
+ .withField(StandardField.JOURNALTITLE, "IEEE Transactions on Industrial Informatics")
+ .withField(StandardField.PAGES, "2346--2356")
+ .withField(StandardField.PUBLISHER, "IEEE")
+ .withField(StandardField.TITLE, "Economic Operation and Quality Control in PV-BES-DG-Based Autonomous System")
+ .withField(StandardField.VOLUME, "16")
+ .withField(StandardField.KEYWORDS, "Batteries, Generators, Economics, Power quality, State of charge, Harmonic analysis, Control systems, Battery, diesel generator (DG), distributed generation, power quality, photovoltaic (PV), voltage source converter (VSC)");
+
+ Files.delete(fileBefore);
+ List changes = removeLinks.cleanup(entry);
+
+ assertEquals(List.of(expectedChange), changes);
+ assertEquals(expectedEntry, entry);
+ }
+
+ @Test
+ void keepLinksToExistingFiles() {
+ LinkedFile fileField = new LinkedFile("", fileBefore.toAbsolutePath(), "");
+ BibEntry expectedEntry = new BibEntry(StandardEntryType.Article)
+ .withField(StandardField.AUTHOR, "Shatakshi Sharma and Bhim Singh and Sukumar Mishra")
+ .withField(StandardField.DATE, "April 2020")
+ .withField(StandardField.YEAR, "2020")
+ .withField(StandardField.DOI, "10.1109/TII.2019.2935531")
+ .withField(StandardField.FILE, FileFieldWriter.getStringRepresentation(List.of(
+ new LinkedFile("", "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8801912", "PDF"),
+ fileField)))
+ .withField(StandardField.ISSUE, "4")
+ .withField(StandardField.ISSN, "1941-0050")
+ .withField(StandardField.JOURNALTITLE, "IEEE Transactions on Industrial Informatics")
+ .withField(StandardField.PAGES, "2346--2356")
+ .withField(StandardField.PUBLISHER, "IEEE")
+ .withField(StandardField.TITLE, "Economic Operation and Quality Control in PV-BES-DG-Based Autonomous System")
+ .withField(StandardField.VOLUME, "16")
+ .withField(StandardField.KEYWORDS, "Batteries, Generators, Economics, Power quality, State of charge, Harmonic analysis, Control systems, Battery, diesel generator (DG), distributed generation, power quality, photovoltaic (PV), voltage source converter (VSC)");
+
+ List changes = removeLinks.cleanup(entry);
+
+ assertEquals(List.of(), changes);
+ assertEquals(expectedEntry, entry);
+ }
+
+ @Test
+ void deleteLinkedFile() throws IOException {
+ LinkedFile fileField = new LinkedFile("", fileBefore.toAbsolutePath(), "");
+
+ // There is only one linked file in entry
+ entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));
+ FieldChange expectedChange = new FieldChange(entry, StandardField.FILE,
+ FileFieldWriter.getStringRepresentation(fileField),
+ null);
+ BibEntry expectedEntry = new BibEntry(StandardEntryType.Article)
+ .withField(StandardField.AUTHOR, "Shatakshi Sharma and Bhim Singh and Sukumar Mishra")
+ .withField(StandardField.DATE, "April 2020")
+ .withField(StandardField.YEAR, "2020")
+ .withField(StandardField.DOI, "10.1109/TII.2019.2935531")
+ .withField(StandardField.ISSUE, "4")
+ .withField(StandardField.ISSN, "1941-0050")
+ .withField(StandardField.JOURNALTITLE, "IEEE Transactions on Industrial Informatics")
+ .withField(StandardField.PAGES, "2346--2356")
+ .withField(StandardField.PUBLISHER, "IEEE")
+ .withField(StandardField.TITLE, "Economic Operation and Quality Control in PV-BES-DG-Based Autonomous System")
+ .withField(StandardField.VOLUME, "16")
+ .withField(StandardField.KEYWORDS, "Batteries, Generators, Economics, Power quality, State of charge, Harmonic analysis, Control systems, Battery, diesel generator (DG), distributed generation, power quality, photovoltaic (PV), voltage source converter (VSC)");
+
+ Files.delete(fileBefore);
+ List changes = removeLinks.cleanup(entry);
+
+ assertEquals(List.of(expectedChange), changes);
+ assertEquals(expectedEntry, entry);
+ }
+}