Skip to content

Fix/copy linked files on entry transfer #13535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6b73b3f
refactor(RightClickMenu): pass libraryTab to createCopySubMenu
UmutAkbayin Jul 6, 2025
f036db2
feat(ClipBoardManager): add static BibDatabaseContext field with gett…
UmutAkbayin Jul 6, 2025
ddb7fff
refactor(ClipBoardManager): change sourceDatabaseContext to instance …
UmutAkbayin Jul 6, 2025
9da934b
feat(RightClickMenu): set sourceBibDatabaseContext from libraryTab
UmutAkbayin Jul 6, 2025
83070d5
feat: add LinkedFileTransferHelper#adjustLinkedFilesForTarget call to…
UmutAkbayin Jul 6, 2025
36cc6af
feat(CopyTo): support file preferences in copy operations
UmutAkbayin Jul 6, 2025
fd355cf
feat(LinkedFileTransferHelper): add new helper class to support Entry…
UmutAkbayin Jul 6, 2025
fb76057
feat(LinkedFileTransferHelper): add helper to check reachability from…
UmutAkbayin Jul 7, 2025
a5a48a7
test(LinkedFileTransferHelperTest): add test cases for LinkedFileTran…
UmutAkbayin Jul 7, 2025
427c12c
feat: adjust relative file paths after entry transfer between libraries
UmutAkbayin Jul 7, 2025
8b52972
feat(ImportHandler): clone entry in importEntryWithDuplicateCheck
UmutAkbayin Jul 12, 2025
83ad967
chore: update LinkedFileTransferHelper#adjustLinkedFilesForTarget cal…
UmutAkbayin Jul 12, 2025
35e97bc
refactor: update LibraryTab#pasteEntry to copy with feedback
UmutAkbayin Jul 12, 2025
bf4b442
feat(LinkedFileTransferHelper): enhance linked file adjustment and co…
UmutAkbayin Jul 12, 2025
86e648c
test(LinkedFileTransferHelperTest): add tests for all three scenarios
UmutAkbayin Jul 13, 2025
24a1e6b
docs(requirements): add file transfer requirements specification
UmutAkbayin Jul 13, 2025
aaebd48
chore: remove unnecessary comments and adjust filed.ms to increment t…
UmutAkbayin Jul 13, 2025
5e32e8b
chore: replace Collection constructors with factories
UmutAkbayin Jul 13, 2025
3e54bc7
chore: remove DisplayNames for test classes
UmutAkbayin Jul 13, 2025
ee96a85
chore: replace assertTrue with assertEquals
UmutAkbayin Jul 13, 2025
2e8125b
refactor: change method signature to have boolean parameter last and …
UmutAkbayin Jul 13, 2025
f2bcebb
refactor: replace IOException with Exception in the throws clause and…
UmutAkbayin Jul 13, 2025
5a23b6f
refactor: remove unused import
UmutAkbayin Jul 13, 2025
f2b46a9
refactor: remove static modifier
UmutAkbayin Jul 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/requirements/filed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# File Transfer Between Bib Entries

### File is reachable and should not be copied

Check failure on line 3 in docs/requirements/filed.md

View workflow job for this annotation

GitHub Actions / Markdown

Heading levels should only increment by one level at a time

docs/requirements/filed.md:3 MD001/heading-increment Heading levels should only increment by one level at a time [Expected: h2; Actual: h3] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md001.md
`req~logic.externalfiles.file-transfer.reachable-no-copy~1`
When a linked file is reachable from the target context, the system must adjust the relative path in the target entry but must not copy the file again.

Needs: impl

### File is not reachable, but the path is the same
`req~logic.externalfiles.file-transfer.not-reachable-same-path~1`
When a linked file is not reachable from the target context, and the relative path in both source and target entry is the same, the file must be copied to the target context.

Needs: impl

### File is not reachable, and a different path is used
`req~logic.externalfiles.file-transfer.not-reachable-different-path~1`
When a linked file is not reachable from the target context, and the relative path differs between source and target entries, the file must be copied and the directory structure must be created to preserve the relative link.

Needs: impl

<!-- markdownlint-disable-file MD022 -->
14 changes: 14 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/ClipBoardManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.List;
import java.util.Optional;

import javafx.application.Platform;
import javafx.scene.control.TextInputControl;
Expand All @@ -19,6 +20,7 @@
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
Expand All @@ -35,6 +37,8 @@ public class ClipBoardManager {

private static final Logger LOGGER = LoggerFactory.getLogger(ClipBoardManager.class);

private BibDatabaseContext sourceDatabaseContext;

private static Clipboard clipboard;
private static java.awt.datatransfer.Clipboard primary;

Expand Down Expand Up @@ -117,6 +121,10 @@ public static String getContentsPrimary() {
return getContents();
}

public Optional<BibDatabaseContext> getSourceBibDatabaseContext() {
return Optional.ofNullable(sourceDatabaseContext);
}

/**
* Puts content onto the system clipboard.
*
Expand Down Expand Up @@ -166,6 +174,12 @@ public void setContent(List<BibEntry> entries, BibEntryTypesManager entryTypesMa
setContent(builder.toString());
}

public void setSourceBibDatabaseContext(BibDatabaseContext context) {
if (context != null) {
sourceDatabaseContext = context;
}
}

private String serializeEntries(List<BibEntry> entries, BibEntryTypesManager entryTypesManager) throws IOException {
CliPreferences preferences = Injector.instantiateModelOrService(CliPreferences.class);
// BibEntry is not Java serializable. Thus, we need to do the serialization manually
Expand Down
60 changes: 48 additions & 12 deletions jabgui/src/main/java/org/jabref/gui/LibraryTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.jabref.gui.collab.DatabaseChangeMonitor;
import org.jabref.gui.dialogs.AutosaveUiManager;
import org.jabref.gui.exporter.SaveDatabaseAction;
import org.jabref.gui.externalfiles.EntryImportHandlerTracker;
import org.jabref.gui.externalfiles.ImportHandler;
import org.jabref.gui.fieldeditors.LinkedFileViewModel;
import org.jabref.gui.importer.actions.OpenDatabaseAction;
Expand All @@ -60,6 +61,7 @@
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.ai.AiService;
import org.jabref.logic.citationstyle.CitationStyleCache;
import org.jabref.logic.externalfiles.LinkedFileTransferHelper;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.FetcherServerException;
Expand Down Expand Up @@ -816,21 +818,55 @@ private int doCopyEntry(List<BibEntry> selectedEntries) {
}
}

public void pasteEntry() {
List<BibEntry> entriesToAdd;
String content = ClipBoardManager.getContents();
entriesToAdd = importHandler.handleBibTeXData(content);
if (entriesToAdd.isEmpty()) {
entriesToAdd = handleNonBibTeXStringData(content);
}
if (entriesToAdd.isEmpty()) {
return;
}
public void pasteEntry() {
String content = ClipBoardManager.getContents();
List<BibEntry> entriesToAdd = importHandler.handleBibTeXData(content);
if (entriesToAdd.isEmpty()) {
entriesToAdd = handleNonBibTeXStringData(content);
}
if (entriesToAdd.isEmpty()) {
return;
}
copyEntriesWithFeedback(entriesToAdd);
}

importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, entriesToAdd);
private void copyEntriesWithFeedback(List<BibEntry> entriesToAdd) {
final List<BibEntry> finalEntriesToAdd = entriesToAdd;

EntryImportHandlerTracker tracker = new EntryImportHandlerTracker(finalEntriesToAdd.size());

tracker.setOnFinish(() -> {
int importedCount = tracker.getImportedCount();
int skippedCount = tracker.getSkippedCount();

String targetName = bibDatabaseContext.getDatabasePath()
.map(path -> path.getFileName().toString())
.orElse(Localization.lang("target library"));

if (importedCount == finalEntriesToAdd.size()) {
dialogService.notify(Localization.lang("Pasted %0 entry(s) to %1",
String.valueOf(importedCount), targetName));
} else if (importedCount == 0) {
dialogService.notify(Localization.lang("No entry was pasted to %0", targetName));
} else {
dialogService.notify(Localization.lang("Pasted %0 entry(s) to %1. %2 were skipped",
String.valueOf(importedCount), targetName, String.valueOf(skippedCount)));
}
});

// Import mit Duplikat-Check durchführen
importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, finalEntriesToAdd, tracker);

// Linked Files anpassen falls Source-Context vorhanden
if (clipBoardManager.getSourceBibDatabaseContext().isPresent()) {
tracker.setOnFinish(() -> LinkedFileTransferHelper
.adjustLinkedFilesForTarget(clipBoardManager.getSourceBibDatabaseContext().get(),
bibDatabaseContext, preferences.getFilePreferences()));
}
}


private List<BibEntry> handleNonBibTeXStringData(String data) {
private List<BibEntry> handleNonBibTeXStringData(String data) {
try {
return this.importHandler.handleStringData(data);
} catch (FetcherException exception) {
Expand Down
8 changes: 8 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/edit/CopyTo.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.externalfiles.EntryImportHandlerTracker;
import org.jabref.gui.externalfiles.ImportHandler;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.externalfiles.LinkedFileTransferHelper;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
Expand All @@ -25,19 +27,22 @@ public class CopyTo extends SimpleCommand {
private final DialogService dialogService;
private final StateManager stateManager;
private final CopyToPreferences copyToPreferences;
private final FilePreferences filePreferences;
private final ImportHandler importHandler;
private final BibDatabaseContext sourceDatabaseContext;
private final BibDatabaseContext targetDatabaseContext;

public CopyTo(DialogService dialogService,
StateManager stateManager,
CopyToPreferences copyToPreferences,
FilePreferences filePreferences,
ImportHandler importHandler,
BibDatabaseContext sourceDatabaseContext,
BibDatabaseContext targetDatabaseContext) {
this.dialogService = dialogService;
this.stateManager = stateManager;
this.copyToPreferences = copyToPreferences;
this.filePreferences = filePreferences;
this.importHandler = importHandler;
this.sourceDatabaseContext = sourceDatabaseContext;
this.targetDatabaseContext = targetDatabaseContext;
Expand Down Expand Up @@ -106,6 +111,9 @@ private void copyEntriesWithFeedback(List<BibEntry> entriesToAdd, BibDatabaseCon
});

importHandler.importEntriesWithDuplicateCheck(targetDatabaseContext, entriesToAdd, tracker);
tracker.setOnFinish(() -> LinkedFileTransferHelper
.adjustLinkedFilesForTarget(sourceDatabaseContext, targetDatabaseContext, filePreferences)
);
}

public Optional<BibEntry> getCrossRefEntry(BibEntry bibEntryToCheck, BibDatabaseContext sourceDatabaseContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext,
}

private void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext, BibEntry entry, DuplicateResolverDialog.DuplicateResolverResult decision, EntryImportHandlerTracker tracker) {
BibEntry entryToInsert = cleanUpEntry(bibDatabaseContext, entry);
BibEntry entryToInsert = (BibEntry) cleanUpEntry(bibDatabaseContext, entry).clone();

BackgroundTask.wrap(() -> findDuplicate(bibDatabaseContext, entryToInsert))
.onFailure(e -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public static ContextMenu create(BibEntryTableViewModel entry,

contextMenu.getItems().addAll(
factory.createMenuItem(StandardActions.COPY, new EditAction(StandardActions.COPY, () -> libraryTab, stateManager, undoManager)),
createCopySubMenu(factory, dialogService, stateManager, preferences, clipBoardManager, abbreviationRepository, taskExecutor),
createCopySubMenu(factory, dialogService, stateManager, preferences, libraryTab, clipBoardManager, abbreviationRepository, taskExecutor),
createCopyToMenu(factory, dialogService, stateManager, preferences, libraryTab, importHandler),
factory.createMenuItem(StandardActions.PASTE, new EditAction(StandardActions.PASTE, () -> libraryTab, stateManager, undoManager)),
factory.createMenuItem(StandardActions.CUT, new EditAction(StandardActions.CUT, () -> libraryTab, stateManager, undoManager)),
Expand Down Expand Up @@ -157,7 +157,8 @@ private static Menu createCopyToMenu(ActionFactory factory,
copyToMenu.getItems().addAll(
factory.createCustomMenuItem(
StandardActions.COPY_TO,
new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, bibDatabaseContext),
new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, bibDatabaseContext),
destinationDatabaseName
)
);
Expand All @@ -171,11 +172,14 @@ private static Menu createCopySubMenu(ActionFactory factory,
DialogService dialogService,
StateManager stateManager,
GuiPreferences preferences,
LibraryTab libraryTab,
ClipBoardManager clipBoardManager,
JournalAbbreviationRepository abbreviationRepository,
TaskExecutor taskExecutor) {
Menu copySpecialMenu = factory.createMenu(StandardActions.COPY_MORE);

clipBoardManager.setSourceBibDatabaseContext(libraryTab.getBibDatabaseContext());

copySpecialMenu.getItems().addAll(
factory.createMenuItem(StandardActions.COPY_TITLE, new CopyMoreAction(StandardActions.COPY_TITLE, dialogService, stateManager, clipBoardManager, preferences, abbreviationRepository)),
factory.createMenuItem(StandardActions.COPY_KEY, new CopyMoreAction(StandardActions.COPY_KEY, dialogService, stateManager, clipBoardManager, preferences, abbreviationRepository)),
Expand Down
17 changes: 11 additions & 6 deletions jabgui/src/test/java/org/jabref/gui/edit/CopyToTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void executeCopyEntriesWithoutCrossRef() {
selectedEntries.add(entry);
when(stateManager.getSelectedEntries()).thenReturn((ObservableList<BibEntry>) selectedEntries);

copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), preferences.getFilePreferences(),
importHandler, sourceDatabaseContext, targetDatabaseContext);

ArgumentCaptor<EntryImportHandlerTracker> trackerCaptor = ArgumentCaptor.forClass(EntryImportHandlerTracker.class);
Expand All @@ -98,7 +98,8 @@ void executeCopyEntriesWithCrossRef() {
selectedEntries.add(entryWithCrossRef);
when(stateManager.getSelectedEntries()).thenReturn((ObservableList<BibEntry>) selectedEntries);

copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);

ArgumentCaptor<EntryImportHandlerTracker> trackerCaptor = ArgumentCaptor.forClass(EntryImportHandlerTracker.class);
copyTo.copyEntriesWithCrossRef(selectedEntries, targetDatabaseContext);
Expand All @@ -111,7 +112,8 @@ void executeCopyEntriesWithCrossRef() {

@Test
void executeGetCrossRefEntry() {
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);

BibEntry result = copyTo.getCrossRefEntry(entryWithCrossRef, sourceDatabaseContext).orElse(null);

Expand All @@ -125,7 +127,8 @@ void executeExecuteWithoutCrossRef() {
when(stateManager.getSelectedEntries()).thenReturn((ObservableList<BibEntry>) selectedEntries);
when(preferences.getCopyToPreferences().getShouldAskForIncludingCrossReferences()).thenReturn(false);

copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);

ArgumentCaptor<EntryImportHandlerTracker> trackerCaptor = ArgumentCaptor.forClass(EntryImportHandlerTracker.class);
copyTo.execute();
Expand All @@ -140,7 +143,8 @@ void executeExecuteWithCrossRefAndUserIncludes() {
when(preferences.getCopyToPreferences().getShouldAskForIncludingCrossReferences()).thenReturn(true);
when(dialogService.showConfirmationDialogWithOptOutAndWait(anyString(), anyString(), anyString(), anyString(), anyString(), any())).thenReturn(true);

copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);

ArgumentCaptor<EntryImportHandlerTracker> trackerCaptor = ArgumentCaptor.forClass(EntryImportHandlerTracker.class);
copyTo.execute();
Expand All @@ -158,7 +162,8 @@ void executeWithCrossRefAndUserExcludes() {
when(preferences.getCopyToPreferences().getShouldAskForIncludingCrossReferences()).thenReturn(true);
when(dialogService.showConfirmationDialogWithOptOutAndWait(anyString(), anyString(), anyString(), anyString(), anyString(), any())).thenReturn(false);

copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);
copyTo = new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(),
preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext);

ArgumentCaptor<EntryImportHandlerTracker> trackerCaptor = ArgumentCaptor.forClass(EntryImportHandlerTracker.class);
copyTo.execute();
Expand Down
11 changes: 4 additions & 7 deletions jablib/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,14 @@
exports org.jabref.logic.pseudonymization;
exports org.jabref.logic.citation.repository;

requires java.base;

requires javafx.base;
requires javafx.base;
requires javafx.graphics; // because of javafx.scene.paint.Color
requires afterburner.fx;
requires com.tobiasdiez.easybind;

// for java.awt.geom.Rectangle2D required by org.jabref.logic.pdf.TextExtractor
requires java.desktop;

// SQL
// SQL
requires java.sql;
requires java.sql.rowset;

Expand Down Expand Up @@ -249,6 +246,6 @@
requires mslinks;
requires org.antlr.antlr4.runtime;
requires org.libreoffice.uno;
requires org.jetbrains.annotations;
// endregion
requires org.jetbrains.annotations;
// endregion
}
Loading
Loading