Skip to content

Commit 32a3c50

Browse files
paudelritijkopporSiedlerchr
authored
Add auto-renaming of linked files on entry data change (#13295)
* Add auto-renaming of linked files on entry data change - Implemented a preference option under Linked files -> Linked file name conventions to enable auto-renaming of files when entry data changes (default: false). - Added functionality to listen for entry change events and rename files if the preference is enabled and the file name matches the defined pattern. - Ensured that no action is taken if the pattern is empty, as the file name would not match. - Considered scenarios where an entry has multiple files attached and handled them appropriately. - Added test cases to verify the new functionality. * Refactor renameToSuggestedName method and add test for file name conflicts * Fix PDF rename binding issue * Add a database request listener for citation key updates * Use finer-grained variable - and skip FILE field Co-authored-by: Christoph <[email protected]> * Some NonNull annotations * Fix check for (1) suffix * Bind to coarse grained listener to reduce renaming count * Fix NPE * More debug * Fix log level for debug messages * Remove exception trace * Add comment why no exception is logged * Compilefix --------- Co-authored-by: Oliver Kopp <[email protected]> Co-authored-by: Christoph <[email protected]>
1 parent 74f74a0 commit 32a3c50

File tree

13 files changed

+322
-25
lines changed

13 files changed

+322
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1111

1212
### Added
1313

14+
- We introduced an option in Preferences under (under Linked files -> Linked file name conventions) to automatically rename linked files when an entry data changes. [#11316](https://github.com/JabRef/jabref/issues/11316)
1415
- We added tooltips (on hover) for 'Library-specific file directory', 'User-specific file directory' and 'LaTeX file directory' fields of the library properties window. [#12269](https://github.com/JabRef/jabref/issues/12269)
1516
- A space is now added by default after citations inserted via the Libre/OpenOffice integration. [#13559](https://github.com/JabRef/jabref/issues/13559)
1617
- We added the option to configure 'Add space after citation' in Libre/OpenOffice panel settings. [#13559](https://github.com/JabRef/jabref/issues/13559)

jabgui/src/main/java/org/jabref/gui/LibraryTab.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.jabref.gui.collab.DatabaseChangeMonitor;
4444
import org.jabref.gui.dialogs.AutosaveUiManager;
4545
import org.jabref.gui.exporter.SaveDatabaseAction;
46+
import org.jabref.gui.externalfiles.AutoRenameFileOnEntryChange;
4647
import org.jabref.gui.externalfiles.ImportHandler;
4748
import org.jabref.gui.fieldeditors.LinkedFileViewModel;
4849
import org.jabref.gui.importer.actions.OpenDatabaseAction;
@@ -128,6 +129,7 @@ public class LibraryTab extends Tab implements CommandSelectionTab {
128129
private FileAnnotationCache annotationCache;
129130
private MainTable mainTable;
130131
private DatabaseNotification databaseNotificationPane;
132+
private AutoRenameFileOnEntryChange autoRenameFileOnEntryChange;
131133

132134
// Indicates whether the tab is loading data using a dataloading task
133135
// The constructors take care to the right true/false assignment during start.
@@ -244,6 +246,9 @@ private void initializeComponentsAndListeners(boolean isDummyContext) {
244246

245247
this.getDatabase().registerListener(new UpdateTimestampListener(preferences));
246248

249+
autoRenameFileOnEntryChange = new AutoRenameFileOnEntryChange(bibDatabaseContext, preferences.getFilePreferences());
250+
coarseChangeFilter.registerListener(autoRenameFileOnEntryChange);
251+
247252
aiService.setupDatabase(bibDatabaseContext);
248253

249254
Platform.runLater(() -> {
@@ -717,6 +722,10 @@ private void onClosed(Event event) {
717722
tableModel.unbind();
718723
}
719724

725+
if (autoRenameFileOnEntryChange != null) {
726+
coarseChangeFilter.unregisterListener(autoRenameFileOnEntryChange);
727+
}
728+
720729
// clean up the groups map
721730
stateManager.clearSelectedGroups(bibDatabaseContext);
722731
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import org.jabref.logic.FilePreferences;
4+
import org.jabref.logic.cleanup.RenamePdfCleanup;
5+
import org.jabref.model.database.BibDatabaseContext;
6+
import org.jabref.model.entry.BibEntry;
7+
import org.jabref.model.entry.event.FieldChangedEvent;
8+
import org.jabref.model.entry.field.StandardField;
9+
10+
import com.google.common.eventbus.Subscribe;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
public class AutoRenameFileOnEntryChange {
15+
private static final Logger LOGGER = LoggerFactory.getLogger(AutoRenameFileOnEntryChange.class);
16+
17+
private final FilePreferences filePreferences;
18+
private final RenamePdfCleanup renamePdfCleanup;
19+
20+
public AutoRenameFileOnEntryChange(BibDatabaseContext bibDatabaseContext, FilePreferences filePreferences) {
21+
this.filePreferences = filePreferences;
22+
renamePdfCleanup = new RenamePdfCleanup(false, () -> bibDatabaseContext, filePreferences);
23+
}
24+
25+
@Subscribe
26+
public void listen(FieldChangedEvent event) {
27+
if (!filePreferences.shouldAutoRenameFilesOnChange()
28+
|| filePreferences.getFileNamePattern().isEmpty()
29+
|| filePreferences.getFileNamePattern() == null) {
30+
return;
31+
}
32+
33+
if (event.getField().equals(StandardField.FILE)) {
34+
return;
35+
}
36+
37+
BibEntry entry = event.getBibEntry();
38+
LOGGER.debug("Field changed for entry {}: {}", entry.getCitationKey().orElse("defaultCitationKey"), event.getField().getName());
39+
if (entry.getFiles().isEmpty()) {
40+
return;
41+
}
42+
renamePdfCleanup.cleanup(entry);
43+
}
44+
}

jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class LinkedFilesTab extends AbstractPreferenceTabView<LinkedFilesTabView
3636
@FXML private TextField autolinkRegexKey;
3737

3838
@FXML private CheckBox fulltextIndex;
39+
@FXML private CheckBox autoRenameFilesOnChange;
3940

4041
@FXML private ComboBox<String> fileNamePattern;
4142
@FXML private TextField fileDirectoryPattern;
@@ -73,6 +74,7 @@ public void initialize() {
7374
autolinkRegexKey.textProperty().bindBidirectional(viewModel.autolinkRegexKeyProperty());
7475
autolinkRegexKey.disableProperty().bind(autolinkUseRegex.selectedProperty().not());
7576
fulltextIndex.selectedProperty().bindBidirectional(viewModel.fulltextIndexProperty());
77+
autoRenameFilesOnChange.selectedProperty().bindBidirectional(viewModel.autoRenameFilesOnChangeProperty());
7678
fileNamePattern.valueProperty().bindBidirectional(viewModel.fileNamePatternProperty());
7779
fileNamePattern.itemsProperty().bind(viewModel.defaultFileNamePatternsProperty());
7880
fileDirectoryPattern.textProperty().bindBidirectional(viewModel.fileDirectoryPatternProperty());

jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTabViewModel.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class LinkedFilesTabViewModel implements PreferenceTabViewModel {
3737
private final ListProperty<String> defaultFileNamePatternsProperty =
3838
new SimpleListProperty<>(FXCollections.observableArrayList(FilePreferences.DEFAULT_FILENAME_PATTERNS));
3939
private final BooleanProperty fulltextIndex = new SimpleBooleanProperty();
40+
private final BooleanProperty autoRenameFilesOnChangeProperty = new SimpleBooleanProperty();
4041
private final StringProperty fileNamePatternProperty = new SimpleStringProperty();
4142
private final StringProperty fileDirectoryPatternProperty = new SimpleStringProperty();
4243
private final BooleanProperty confirmLinkedFileDeleteProperty = new SimpleBooleanProperty();
@@ -82,6 +83,7 @@ public void setValues() {
8283
useMainFileDirectoryProperty.setValue(!filePreferences.shouldStoreFilesRelativeToBibFile());
8384
useBibLocationAsPrimaryProperty.setValue(filePreferences.shouldStoreFilesRelativeToBibFile());
8485
fulltextIndex.setValue(filePreferences.shouldFulltextIndexLinkedFiles());
86+
autoRenameFilesOnChangeProperty.setValue(filePreferences.shouldAutoRenameFilesOnChange());
8587
fileNamePatternProperty.setValue(filePreferences.getFileNamePattern());
8688
fileDirectoryPatternProperty.setValue(filePreferences.getFileDirectoryPattern());
8789
confirmLinkedFileDeleteProperty.setValue(filePreferences.confirmDeleteLinkedFile());
@@ -104,6 +106,7 @@ public void storeSettings() {
104106
// External files preferences / Attached files preferences / File preferences
105107
filePreferences.setMainFileDirectory(mainFileDirectoryProperty.getValue());
106108
filePreferences.setStoreFilesRelativeToBibFile(useBibLocationAsPrimaryProperty.getValue());
109+
filePreferences.setAutoRenameFilesOnChange(autoRenameFilesOnChangeProperty.getValue());
107110
filePreferences.setFileNamePattern(fileNamePatternProperty.getValue());
108111
filePreferences.setFileDirectoryPattern(fileDirectoryPatternProperty.getValue());
109112
filePreferences.setFulltextIndexLinkedFiles(fulltextIndex.getValue());
@@ -179,6 +182,10 @@ public ListProperty<String> defaultFileNamePatternsProperty() {
179182
return defaultFileNamePatternsProperty;
180183
}
181184

185+
public BooleanProperty autoRenameFilesOnChangeProperty() {
186+
return autoRenameFilesOnChangeProperty;
187+
}
188+
182189
public StringProperty fileNamePatternProperty() {
183190
return fileNamePatternProperty;
184191
}

jabgui/src/main/resources/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<CheckBox fx:id="fulltextIndex" text="%Automatically index all linked files for fulltext search"/>
6666

6767
<Label styleClass="sectionHeader" text="%Linked file name conventions"/>
68+
<CheckBox fx:id="autoRenameFilesOnChange" text="%Auto rename files if entry changes"/>
6869
<GridPane hgap="4.0" vgap="4.0">
6970
<columnConstraints>
7071
<ColumnConstraints hgrow="SOMETIMES" percentWidth="30.0"/>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.util.List;
7+
8+
import org.jabref.gui.preferences.GuiPreferences;
9+
import org.jabref.logic.FilePreferences;
10+
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
11+
import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns;
12+
import org.jabref.model.database.BibDatabase;
13+
import org.jabref.model.database.BibDatabaseContext;
14+
import org.jabref.model.entry.BibEntry;
15+
import org.jabref.model.entry.LinkedFile;
16+
import org.jabref.model.entry.event.FieldChangedEvent;
17+
import org.jabref.model.entry.field.StandardField;
18+
import org.jabref.model.entry.types.StandardEntryType;
19+
import org.jabref.model.metadata.MetaData;
20+
21+
import com.google.common.eventbus.Subscribe;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.io.TempDir;
25+
26+
import static org.jabref.logic.citationkeypattern.CitationKeyGenerator.DEFAULT_UNWANTED_CHARACTERS;
27+
import static org.junit.jupiter.api.Assertions.assertEquals;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
import static org.mockito.Mockito.mock;
30+
import static org.mockito.Mockito.when;
31+
32+
class AutoRenameFileOnEntryChangeTest {
33+
private FilePreferences filePreferences;
34+
private BibEntry entry;
35+
private Path tempDir;
36+
37+
@BeforeEach
38+
void setUp(@TempDir Path tempDir) {
39+
this.tempDir = tempDir;
40+
MetaData metaData = new MetaData();
41+
metaData.setLibrarySpecificFileDirectory(tempDir.toString());
42+
BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(new BibDatabase(), metaData);
43+
GlobalCitationKeyPatterns keyPattern = GlobalCitationKeyPatterns.fromPattern("[auth][year]");
44+
GuiPreferences guiPreferences = mock(GuiPreferences.class);
45+
filePreferences = mock(FilePreferences.class);
46+
CitationKeyPatternPreferences patternPreferences = new CitationKeyPatternPreferences(
47+
false,
48+
true,
49+
false,
50+
CitationKeyPatternPreferences.KeySuffix.SECOND_WITH_A,
51+
"",
52+
"",
53+
DEFAULT_UNWANTED_CHARACTERS,
54+
keyPattern,
55+
"",
56+
',');
57+
58+
when(guiPreferences.getCitationKeyPatternPreferences()).thenReturn(patternPreferences);
59+
when(guiPreferences.getFilePreferences()).thenReturn(filePreferences);
60+
when(filePreferences.shouldStoreFilesRelativeToBibFile()).thenReturn(true);
61+
when(filePreferences.getFileNamePattern()).thenReturn("[bibtexkey]");
62+
63+
entry = new BibEntry(StandardEntryType.Article).withCitationKey("oldKey2081")
64+
.withField(StandardField.AUTHOR, "oldKey")
65+
.withField(StandardField.YEAR, "2081");
66+
67+
bibDatabaseContext.getDatabase().insertEntry(entry);
68+
AutoRenameFileOnEntryChange autoRenameFileOnEntryChange = new AutoRenameFileOnEntryChange(bibDatabaseContext, guiPreferences.getFilePreferences());
69+
bibDatabaseContext.getDatabase().registerListener(autoRenameFileOnEntryChange);
70+
71+
// Update citation-key when author/year changes
72+
bibDatabaseContext.getDatabase().registerListener(new Object() {
73+
@Subscribe
74+
public void listen(FieldChangedEvent event) {
75+
if (event.getField().equals(StandardField.AUTHOR)) {
76+
String author = event.getNewValue();
77+
String year = entry.getField(StandardField.YEAR).orElse("");
78+
entry.setCitationKey(author + year);
79+
} else if (event.getField().equals(StandardField.YEAR)) {
80+
String author = entry.getField(StandardField.AUTHOR).orElse("");
81+
String year = event.getNewValue();
82+
entry.setCitationKey(author + year);
83+
}
84+
}
85+
});
86+
}
87+
88+
@Test
89+
void noFileRenameByDefault() throws IOException {
90+
Files.createFile(tempDir.resolve("oldKey2081.pdf"));
91+
entry.setFiles(List.of(new LinkedFile("", "oldKey2081.pdf", "PDF")));
92+
entry.setField(StandardField.AUTHOR, "newKey");
93+
94+
assertEquals("oldKey2081.pdf", entry.getFiles().getFirst().getLink());
95+
assertTrue(Files.exists(tempDir.resolve("oldKey2081.pdf")));
96+
}
97+
98+
@Test
99+
void noFileRenameOnEmptyFilePattern() throws IOException {
100+
Files.createFile(tempDir.resolve("oldKey2081.pdf"));
101+
entry.setFiles(List.of(new LinkedFile("", "oldKey2081.pdf", "PDF")));
102+
when(filePreferences.getFileNamePattern()).thenReturn("");
103+
when(filePreferences.shouldAutoRenameFilesOnChange()).thenReturn(true);
104+
entry.setField(StandardField.AUTHOR, "newKey");
105+
106+
assertEquals("oldKey2081.pdf", entry.getFiles().getFirst().getLink());
107+
assertTrue(Files.exists(tempDir.resolve("oldKey2081.pdf")));
108+
}
109+
110+
@Test
111+
void singleFileRenameOnEntryChange() throws IOException {
112+
Files.createFile(tempDir.resolve("oldKey2081.pdf"));
113+
entry.setFiles(List.of(new LinkedFile("", "oldKey2081.pdf", "PDF")));
114+
when(filePreferences.shouldAutoRenameFilesOnChange()).thenReturn(true);
115+
116+
// change author only
117+
entry.setField(StandardField.AUTHOR, "newKey");
118+
assertEquals("newKey2081.pdf", entry.getFiles().getFirst().getLink());
119+
assertTrue(Files.exists(tempDir.resolve("newKey2081.pdf")));
120+
121+
// change year only
122+
entry.setField(StandardField.YEAR, "2082");
123+
assertEquals("newKey2082.pdf", entry.getFiles().getFirst().getLink());
124+
assertTrue(Files.exists(tempDir.resolve("newKey2082.pdf")));
125+
}
126+
127+
@Test
128+
void multipleFilesRenameOnEntryChange() throws IOException {
129+
// create multiple entries
130+
List<String> fileNames = List.of(
131+
"oldKey2081.pdf",
132+
"oldKey2081.jpg",
133+
"oldKey2081.csv",
134+
"oldKey2081.doc",
135+
"oldKey2081.docx"
136+
);
137+
138+
for (String fileName : fileNames) {
139+
Path filePath = tempDir.resolve(fileName);
140+
Files.createFile(filePath);
141+
}
142+
143+
LinkedFile pdfLinkedFile = new LinkedFile("", "oldKey2081.pdf", "PDF");
144+
LinkedFile jpgLinkedFile = new LinkedFile("", "oldKey2081.jpg", "JPG");
145+
LinkedFile csvLinkedFile = new LinkedFile("", "oldKey2081.csv", "CSV");
146+
LinkedFile docLinkedFile = new LinkedFile("", "oldKey2081.doc", "DOC");
147+
LinkedFile docxLinkedFile = new LinkedFile("", "oldKey2081.docx", "DOCX");
148+
149+
entry.setFiles(List.of(pdfLinkedFile, jpgLinkedFile, csvLinkedFile, docLinkedFile, docxLinkedFile));
150+
when(filePreferences.shouldAutoRenameFilesOnChange()).thenReturn(true);
151+
152+
// Change author only
153+
entry.setField(StandardField.AUTHOR, "newKey");
154+
assertTrue(Files.exists(tempDir.resolve("newKey2081.pdf")));
155+
assertTrue(Files.exists(tempDir.resolve("newKey2081.jpg")));
156+
assertTrue(Files.exists(tempDir.resolve("newKey2081.csv")));
157+
assertTrue(Files.exists(tempDir.resolve("newKey2081.doc")));
158+
assertTrue(Files.exists(tempDir.resolve("newKey2081.docx")));
159+
160+
// change year only
161+
entry.setField(StandardField.YEAR, "2082");
162+
assertTrue(Files.exists(tempDir.resolve("newKey2082.pdf")));
163+
assertTrue(Files.exists(tempDir.resolve("newKey2082.jpg")));
164+
assertTrue(Files.exists(tempDir.resolve("newKey2082.csv")));
165+
assertTrue(Files.exists(tempDir.resolve("newKey2082.doc")));
166+
assertTrue(Files.exists(tempDir.resolve("newKey2082.docx")));
167+
}
168+
169+
@Test
170+
void shouldHandleFileNameConflicts() throws IOException {
171+
// files that may or may not be linked to another entry
172+
Files.createFile(tempDir.resolve("newKey2081.pdf"));
173+
Files.createFile(tempDir.resolve("newKey2081 (1).pdf"));
174+
175+
Files.createFile(tempDir.resolve("oldKey2081.pdf"));
176+
entry.setFiles(List.of(new LinkedFile("", "oldKey2081.pdf", "PDF")));
177+
when(filePreferences.shouldAutoRenameFilesOnChange()).thenReturn(true);
178+
179+
entry.setField(StandardField.AUTHOR, "newKey");
180+
assertTrue(Files.exists(tempDir.resolve("newKey2081.pdf")));
181+
assertTrue(Files.exists(tempDir.resolve("newKey2081 (1).pdf")));
182+
assertTrue(Files.exists(tempDir.resolve("newKey2081 (2).pdf")));
183+
assertEquals("newKey2081 (2).pdf", entry.getFiles().getFirst().getLink());
184+
}
185+
}

jablib/src/main/java/org/jabref/logic/FilePreferences.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class FilePreferences {
2222
private final StringProperty userAndHost = new SimpleStringProperty();
2323
private final SimpleStringProperty mainFileDirectory = new SimpleStringProperty();
2424
private final BooleanProperty storeFilesRelativeToBibFile = new SimpleBooleanProperty();
25+
private final BooleanProperty autoRenameFilesOnChange = new SimpleBooleanProperty();
2526
private final StringProperty fileNamePattern = new SimpleStringProperty();
2627
private final StringProperty fileDirectoryPattern = new SimpleStringProperty();
2728
private final BooleanProperty downloadLinkedFiles = new SimpleBooleanProperty();
@@ -39,6 +40,7 @@ public class FilePreferences {
3940
public FilePreferences(String userAndHost,
4041
String mainFileDirectory,
4142
boolean storeFilesRelativeToBibFile,
43+
boolean autoRenameFilesOnChange,
4244
String fileNamePattern,
4345
String fileDirectoryPattern,
4446
boolean downloadLinkedFiles,
@@ -55,6 +57,7 @@ public FilePreferences(String userAndHost,
5557
this.userAndHost.setValue(userAndHost);
5658
this.mainFileDirectory.setValue(mainFileDirectory);
5759
this.storeFilesRelativeToBibFile.setValue(storeFilesRelativeToBibFile);
60+
this.autoRenameFilesOnChange.setValue(autoRenameFilesOnChange);
5861
this.fileNamePattern.setValue(fileNamePattern);
5962
this.fileDirectoryPattern.setValue(fileDirectoryPattern);
6063
this.downloadLinkedFiles.setValue(downloadLinkedFiles);
@@ -106,6 +109,18 @@ public void setStoreFilesRelativeToBibFile(boolean shouldStoreFilesRelativeToBib
106109
this.storeFilesRelativeToBibFile.set(shouldStoreFilesRelativeToBibFile);
107110
}
108111

112+
public boolean shouldAutoRenameFilesOnChange() {
113+
return autoRenameFilesOnChange.get();
114+
}
115+
116+
public BooleanProperty autoRenameFilesOnChangeProperty() {
117+
return autoRenameFilesOnChange;
118+
}
119+
120+
public void setAutoRenameFilesOnChange(boolean autoRenameFilesOnChange) {
121+
this.autoRenameFilesOnChange.set(autoRenameFilesOnChange);
122+
}
123+
109124
public String getFileNamePattern() {
110125
return fileNamePattern.get();
111126
}

jablib/src/main/java/org/jabref/logic/cleanup/RenamePdfCleanup.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public List<FieldChange> cleanup(BibEntry entry) {
4848
changed = true;
4949
}
5050
} catch (IOException exception) {
51-
LOGGER.error("Error while renaming file {}", file.getLink(), exception);
51+
// There is no exception logged here, because the stack trace can get very large (and is not helpful)
52+
// The only "real" information lost is i) the absolute path of the source file and ii) the absolute path of the target file.
53+
LOGGER.error("Error while renaming file {}", file.getLink());
5254
}
5355
}
5456

0 commit comments

Comments
 (0)