diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a596a3abb..ac6f98c6f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We enhanced the dialog for adding new fields in the content selector with a selection box containing a list of standard fields. [#10912](https://github.com/JabRef/jabref/pull/10912) - We store the citation relations in an LRU cache to avoid bloating the memory and out-of-memory exceptions. [#10958](https://github.com/JabRef/jabref/issues/10958) - Keywords filed are now displayed as tags. [#10910](https://github.com/JabRef/jabref/pull/10910) +- When pasting, string constants are automatically added to the library database, while referenced constants are added to the clipboard during copying. [#10872](https://github.com/JabRef/jabref/issues/10872) ### Fixed diff --git a/buildres/abbrv.jabref.org b/buildres/abbrv.jabref.org index 8fbad5a1285..1a8ca63172f 160000 --- a/buildres/abbrv.jabref.org +++ b/buildres/abbrv.jabref.org @@ -1 +1 @@ -Subproject commit 8fbad5a1285926b177803087b35b0eb6b0fd0142 +Subproject commit 1a8ca63172f96b77632810bb726dbc6a2df7ac7e diff --git a/src/main/java/org/jabref/gui/ClipBoardManager.java b/src/main/java/org/jabref/gui/ClipBoardManager.java index 792c4733384..f90dc1b0f36 100644 --- a/src/main/java/org/jabref/gui/ClipBoardManager.java +++ b/src/main/java/org/jabref/gui/ClipBoardManager.java @@ -21,6 +21,7 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; @@ -155,14 +156,23 @@ public void setContent(String string) { } public void setContent(List entries, BibEntryTypesManager entryTypesManager) throws IOException { - final ClipboardContent content = new ClipboardContent(); - BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), entryTypesManager); - String serializedEntries = writer.serializeAll(entries, BibDatabaseMode.BIBTEX); + String serializedEntries = serializeEntries(entries, entryTypesManager); + setContent(serializedEntries); + } + + public void setContent(List entries, BibEntryTypesManager entryTypesManager, List stringConstants) throws IOException { + StringBuilder builder = new StringBuilder(); + stringConstants.forEach(strConst -> builder.append(strConst.getParsedSerialization() == null ? "" : strConst.getParsedSerialization())); + String serializedEntries = serializeEntries(entries, entryTypesManager); + builder.append(serializedEntries); + setContent(builder.toString()); + } + + private String serializeEntries(List entries, BibEntryTypesManager entryTypesManager) throws IOException { // BibEntry is not Java serializable. Thus, we need to do the serialization manually // At reading of the clipboard in JabRef, we parse the plain string in all cases, so we don't need to flag we put BibEntries here // Furthermore, storing a string also enables other applications to work with the data - content.putString(serializedEntries); - clipboard.setContent(content); - setPrimaryClipboardContent(content); + BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), entryTypesManager); + return writer.serializeAll(entries, BibDatabaseMode.BIBTEX); } } diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 18ac20841ee..f0ce9a34c37 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -18,6 +18,7 @@ import org.jabref.gui.StateManager; import org.jabref.gui.duplicationFinder.DuplicateResolverDialog; import org.jabref.gui.fieldeditors.LinkedFileViewModel; +import org.jabref.gui.libraryproperties.constants.ConstantsItemModel; import org.jabref.gui.undo.UndoableInsertEntries; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.DefaultTaskExecutor; @@ -40,7 +41,9 @@ import org.jabref.logic.util.io.FileUtil; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.KeyCollisionException; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibtexString; import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.ArXivIdentifier; @@ -311,13 +314,31 @@ private void generateKeys(List entries) { public List handleBibTeXData(String entries) { BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), fileUpdateMonitor); try { - return parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); + List result = parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); + List stringConstants = parser.getStringValues(); + importStringConstantsWithDuplicateCheck(stringConstants); + return result; } catch (ParseException ex) { LOGGER.error("Could not paste", ex); return Collections.emptyList(); } } + public void importStringConstantsWithDuplicateCheck(List stringConstants) { + for (BibtexString stringConstantToAdd : stringConstants) { + try { + ConstantsItemModel checker = new ConstantsItemModel(stringConstantToAdd.getName(), stringConstantToAdd.getContent()); + if (checker.combinedValidationValidProperty().get()) { + bibDatabaseContext.getDatabase().addString(stringConstantToAdd); + } else { + dialogService.showErrorDialogAndWait(Localization.lang("Pasted string constant \"%0\" was not added because it is not a valid string constant", stringConstantToAdd.getName())); + } + } catch (KeyCollisionException ex) { + dialogService.showErrorDialogAndWait(Localization.lang("Pasted string constant %0 was not imported because it already exists in this library", stringConstantToAdd.getName())); + } + } + } + public List handleStringData(String data) throws FetcherException { if ((data == null) || data.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java index 36f4ddeff07..62de209074c 100644 --- a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java +++ b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java @@ -1,9 +1,9 @@ package org.jabref.gui.libraryproperties.constants; import java.util.Comparator; +import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ListProperty; @@ -86,9 +86,12 @@ private ConstantsItemModel convertFromBibTexString(BibtexString bibtexString) { @Override public void storeSettings() { - databaseContext.getDatabase().setStrings(stringsListProperty.stream() - .map(this::fromBibtexStringViewModel) - .collect(Collectors.toList())); + List strings = stringsListProperty.stream() + .map(this::fromBibtexStringViewModel) + .toList(); + strings.forEach(string -> string.setParsedSerialization("@String{" + + string.getName() + " = " + string.getContent() + "}\n")); + databaseContext.getDatabase().setStrings(strings); } private BibtexString fromBibtexStringViewModel(ConstantsItemModel viewModel) { diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index e778fa13425..230ad5c23e7 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -51,6 +51,7 @@ import org.jabref.model.database.event.EntriesAddedEvent; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -257,8 +258,13 @@ public void copy() { List selectedEntries = getSelectedEntries(); if (!selectedEntries.isEmpty()) { + List stringConstants = getUsedStringValues(selectedEntries); try { - clipBoardManager.setContent(selectedEntries, entryTypesManager); + if (!stringConstants.isEmpty()) { + clipBoardManager.setContent(selectedEntries, entryTypesManager, stringConstants); + } else { + clipBoardManager.setContent(selectedEntries, entryTypesManager); + } dialogService.notify(Localization.lang("Copied %0 entry(ies)", selectedEntries.size())); } catch (IOException e) { LOGGER.error("Error while copying selected entries to clipboard.", e); @@ -489,4 +495,8 @@ private Optional findEntry(BibEntry entry) { .filter(viewModel -> viewModel.getEntry().equals(entry)) .findFirst(); } + + private List getUsedStringValues(List entries) { + return database.getDatabase().getUsedStrings(entries).stream().toList(); + } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index 0a32dd95989..33cb3f6ba7f 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -117,6 +117,10 @@ public List parseEntries(InputStream inputStream) throws ParseExceptio } } + public List getStringValues() { + return database.getStringValues().stream().toList(); + } + public Optional parseSingleEntry(String bibtexString) throws ParseException { return parseEntries(bibtexString).stream().findFirst(); } diff --git a/src/main/java/org/jabref/model/entry/BibtexString.java b/src/main/java/org/jabref/model/entry/BibtexString.java index d8e83fb6477..3515e0584e9 100644 --- a/src/main/java/org/jabref/model/entry/BibtexString.java +++ b/src/main/java/org/jabref/model/entry/BibtexString.java @@ -158,6 +158,9 @@ public String getUserComments() { public Object clone() { BibtexString clone = new BibtexString(name, content); clone.setId(id); + if (parsedSerialization != null) { + clone.setParsedSerialization(parsedSerialization); + } return clone; } diff --git a/src/main/resources/csl-styles b/src/main/resources/csl-styles index 1bb9097598f..fab696b6f36 160000 --- a/src/main/resources/csl-styles +++ b/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit 1bb9097598f2d85d3e5997702bddc2d73ecfb584 +Subproject commit fab696b6f365a770547df90b84a54691462b7c21 diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 26eb731b57d..e8dff158caf 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2643,3 +2643,7 @@ Source\ URL=Source URL Redownload\ file=Redownload file Redownload\ missing\ files=Redownload missing files Redownload\ missing\ files\ for\ current\ library?=Redownload missing files for current library? + +Pasted\ string\ constant\ "%0"\ was\ not\ added\ because\ it\ is\ not\ a\ valid\ string\ constant=Pasted string constant "%0" was not added because it is not a valid string constant +Pasted\ string\ constant\ %0\ was\ not\ imported\ because\ it\ already\ exists\ in\ this\ library=Pasted string constant %0 was not imported because it already exists in this library + diff --git a/src/test/java/org/jabref/gui/ClipBoardManagerTest.java b/src/test/java/org/jabref/gui/ClipBoardManagerTest.java new file mode 100644 index 00000000000..08555035686 --- /dev/null +++ b/src/test/java/org/jabref/gui/ClipBoardManagerTest.java @@ -0,0 +1,133 @@ +package org.jabref.gui; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.input.Clipboard; + +import org.jabref.architecture.AllowedToUseAwt; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@AllowedToUseAwt("Requires AWT for clipboard access") +public class ClipBoardManagerTest { + + private BibEntryTypesManager entryTypesManager; + private ClipBoardManager clipBoardManager; + + @BeforeEach + void setUp() { + // create preference service mock + PreferencesService preferencesService = mock(PreferencesService.class); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + List fields = Arrays.asList(StandardField.URL); + ObservableList nonWrappableFields = FXCollections.observableArrayList(fields); + // set up mock behaviours for preferences service + when(fieldPreferences.getNonWrappableFields()).thenReturn(nonWrappableFields); + when(preferencesService.getFieldPreferences()).thenReturn(fieldPreferences); + + // create mock clipboard + Clipboard clipboard = mock(Clipboard.class); + // create primary clipboard and set a temporary value + StringSelection selection = new StringSelection("test"); + java.awt.datatransfer.Clipboard clipboardPrimary = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboardPrimary.setContents(selection, selection); + + // create mock entry manager and set up behaviour for mock + entryTypesManager = mock(BibEntryTypesManager.class); + BibEntryType entryTypeMock = mock(BibEntryType.class); + when(entryTypesManager.enrich(any(), any())).thenReturn(Optional.of(entryTypeMock)); + // initialize a clipBoardManager + clipBoardManager = new ClipBoardManager(clipboard, clipboardPrimary, preferencesService); + } + + @DisplayName("Check that the ClipBoardManager can set a bibentry as its content from the clipboard") + @Test + void copyStringBibEntry() throws IOException { + // Arrange + String expected = "@Article{,\n author = {Claudepierre, S. G.},\n journal = {IEEE},\n}"; + + // create BibEntry + BibEntry bibEntry = new BibEntry(); + // construct an entry + bibEntry.setType(StandardEntryType.Article); + bibEntry.setField(StandardField.JOURNAL, "IEEE"); + bibEntry.setField(StandardField.AUTHOR, "Claudepierre, S. G."); + // add entry to list + List bibEntries = new ArrayList<>(); + bibEntries.add(bibEntry); + + // Act + clipBoardManager.setContent(bibEntries, entryTypesManager); + + // Assert + String actual = ClipBoardManager.getContentsPrimary(); + // clean strings + actual = actual.replaceAll("\\s+", " ").trim(); + expected = expected.replaceAll("\\s+", " ").trim(); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Check that the ClipBoardManager can handle a bibentry with string constants correctly from the clipboard") + void copyStringBibEntryWithStringConstants() throws IOException { + // Arrange + String expected = "@String{grl = \"Geophys. Res. Lett.\"}@Article{,\n" + " author = {Claudepierre, S. G.},\n" + + " journal = {grl},\n" + "}"; + // create BibEntry + BibEntry bibEntry = new BibEntry(); + // construct an entry + bibEntry.setType(StandardEntryType.Article); + bibEntry.setField(StandardField.JOURNAL, "grl"); + bibEntry.setField(StandardField.AUTHOR, "Claudepierre, S. G."); + // add entry to list + List bibEntries = new ArrayList<>(); + bibEntries.add(bibEntry); + + // string constants + List constants = new ArrayList<>(); + + // Mock BibtexString + BibtexString bibtexString = mock(BibtexString.class); + + // define return value for getParsedSerialization() + when(bibtexString.getParsedSerialization()).thenReturn("@String{grl = \"Geophys. Res. Lett.\"}"); + // add the constant + constants.add(bibtexString); + + // Act + clipBoardManager.setContent(bibEntries, entryTypesManager, constants); + + // Assert + String actual = ClipBoardManager.getContentsPrimary(); + // clean strings + actual = actual.replaceAll("\\s+", " ").trim(); + expected = expected.replaceAll("\\s+", " ").trim(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java b/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java index 60311301dee..d7a75a60be3 100644 --- a/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java +++ b/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java @@ -1,6 +1,7 @@ package org.jabref.gui.libraryproperties.constants; import java.util.List; +import java.util.stream.Stream; import javafx.beans.property.StringProperty; @@ -70,4 +71,65 @@ void stringsListPropertyResorting() { assertEquals(expected, actual); } + + @Test + @DisplayName("Check that the storeSettings method store settings on the model") + void storeSettingsTest() { + // Setup + // create a bibdatabse + BibDatabase db = new BibDatabase(); + BibDatabaseContext context = new BibDatabaseContext(db); + List expected = List.of("KTH", "Royal Institute of Technology"); + // initialize a constantsPropertiesViewModel + ConstantsPropertiesViewModel model = new ConstantsPropertiesViewModel(context, service, filePreferences); + + // construct value to store in model + var stringsList = model.stringsListProperty(); + stringsList.add(new ConstantsItemModel("KTH", "Royal Institute of Technology")); + + // Act + model.storeSettings(); + + // Assert + // get the names stored + List names = context.getDatabase().getStringValues().stream() + .map(BibtexString::getName).toList(); + // get the content stored + List content = context.getDatabase().getStringValues().stream() + .map(BibtexString::getContent).toList(); + + List actual = Stream.concat(names.stream(), content.stream()).toList(); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Check that the storeSettings method can identify string constants") + void storeSettingsWithStringConstantTest() { + // Setup + // create a bibdatabse + BibDatabase db = new BibDatabase(); + BibDatabaseContext context = new BibDatabaseContext(db); + List expected = List.of("@String{KTH = Royal Institute of Technology}"); + // initialize a constantsPropertiesViewModel + ConstantsPropertiesViewModel model = new ConstantsPropertiesViewModel(context, service, filePreferences); + + // construct value to store in model + var stringsList = model.stringsListProperty(); + stringsList.add(new ConstantsItemModel("KTH", "Royal Institute of Technology")); + + // Act + model.storeSettings(); + + // Assert + // get string the constants through parsedSerialization() method + List actual = context.getDatabase().getStringValues().stream() + .map(BibtexString::getParsedSerialization).toList(); + + // get the first value and clean strings + String actual_value = actual.getFirst().replaceAll("\\s+", " ").trim(); + String expected_value = expected.getFirst().replaceAll("\\s+", " ").trim(); + + assertEquals(expected_value, actual_value); + } }