diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2c95ffe47..44df8cf9b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We introduced a settings parameter to manage citations' relations local storage time-to-live with a default value set to 30 days. [#11189](https://github.com/JabRef/jabref/issues/11189) - We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842) +- When adding an entry to a library, a warning is displayed if said entry already exists in an active library. [#13261](https://github.com/JabRef/jabref/issues/13261) - We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375) - We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389) diff --git a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryView.java b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryView.java index d1e52bbc5c9..fe6e6e7a4ad 100644 --- a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryView.java +++ b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryView.java @@ -10,12 +10,14 @@ import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; import javafx.scene.control.TitledPane; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; @@ -27,6 +29,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.LibraryTab; import org.jabref.gui.StateManager; +import org.jabref.gui.fieldeditors.EditorValidator; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.search.SearchType; import org.jabref.gui.util.BaseDialog; @@ -104,6 +107,8 @@ public class NewEntryView extends BaseDialog { @FXML private TilePane entryCustom; @FXML private TextField idText; + @FXML private Tooltip idTextTooltip; + @FXML private Hyperlink idJumpLink; @FXML private RadioButton idLookupGuess; @FXML private RadioButton idLookupSpecify; @FXML private ComboBox idFetcher; @@ -260,7 +265,6 @@ private void initializeLookupIdentifier() { // method (each automatically independently, or all through the same fetcher). idText.setPromptText(Localization.lang("Enter the reference identifier to search for.")); idText.textProperty().bindBidirectional(viewModel.idTextProperty()); - final String clipboardText = ClipBoardManager.getContents().trim(); ToggleGroup toggleGroup = new ToggleGroup(); idLookupGuess.setToggleGroup(toggleGroup); @@ -272,6 +276,8 @@ private void initializeLookupIdentifier() { idLookupSpecify.selectedProperty().set(true); } + viewModel.populateDOICache(); + // [impl->req~newentry.clipboard.autofocus~1] Optional validClipboardId = extractValidIdentifierFromClipboard(); if (validClipboardId.isPresent()) { @@ -301,8 +307,24 @@ private void initializeLookupIdentifier() { idFetcher.setValue(initialFetcher); idFetcher.setOnAction(_ -> preferences.setLatestIdFetcher(idFetcher.getValue().getName())); + idJumpLink.visibleProperty().bind(viewModel.duplicateDoiValidatorStatus().validProperty().not()); idErrorInvalidText.visibleProperty().bind(viewModel.idTextValidatorProperty().not()); + idErrorInvalidText.managedProperty().bind(viewModel.idTextValidatorProperty().not()); idErrorInvalidFetcher.visibleProperty().bind(idLookupSpecify.selectedProperty().and(viewModel.idFetcherValidatorProperty().not())); + + idJumpLink.setOnAction(_ -> libraryTab.showAndEdit(viewModel.getDuplicateEntry())); + + viewModel.duplicateDoiValidatorStatus().validProperty().addListener((_, _, isValid) -> { + if (isValid) { + Tooltip.install(idText, idTextTooltip); + } else { + Tooltip.uninstall(idText, idTextTooltip); + } + }); + + TextInputControl textInput = idText; + EditorValidator validator = new EditorValidator(this.guiPreferences); + validator.configureValidation(viewModel.duplicateDoiValidatorStatus(), textInput); } private void initializeInterpretCitations() { diff --git a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java index 601c3314638..65f71a5c8c6 100644 --- a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java @@ -1,6 +1,9 @@ package org.jabref.gui.newentry; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -38,12 +41,17 @@ import org.jabref.logic.importer.plaincitation.RuleBasedPlainCitationParser; import org.jabref.logic.importer.plaincitation.SeveralPlainCitationParser; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.layout.LayoutFormatter; +import org.jabref.logic.layout.format.DOIStrip; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; import de.saxsys.mvvmfx.utils.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +73,7 @@ public class NewEntryViewModel { private final StringProperty idText; private final Validator idTextValidator; + private final Validator duplicateDoiValidator; private final ListProperty idFetchers; private final ObjectProperty idFetcher; private final Validator idFetcherValidator; @@ -79,6 +88,8 @@ public class NewEntryViewModel { private final StringProperty bibtexText; private final Validator bibtexTextValidator; private Task>> bibtexWorker; + private final Map doiCache; + private BibEntry duplicateEntry; public NewEntryViewModel(GuiPreferences preferences, LibraryTab libraryTab, @@ -97,12 +108,16 @@ public NewEntryViewModel(GuiPreferences preferences, executing = new SimpleBooleanProperty(false); executedSuccessfully = new SimpleBooleanProperty(false); + doiCache = new HashMap<>(); idText = new SimpleStringProperty(); idTextValidator = new FunctionBasedValidator<>( idText, StringUtil::isNotBlank, ValidationMessage.error(Localization.lang("You must specify an identifier."))); + duplicateDoiValidator = new FunctionBasedValidator<>( + idText, + input -> checkDOI(input).orElse(null)); idFetchers = new SimpleListProperty<>(FXCollections.observableArrayList()); idFetchers.addAll(WebFetchers.getIdBasedFetchers(preferences.getImportFormatPreferences(), preferences.getImporterPreferences())); idFetcher = new SimpleObjectProperty<>(); @@ -130,6 +145,39 @@ public NewEntryViewModel(GuiPreferences preferences, bibtexWorker = null; } + public void populateDOICache() { + doiCache.clear(); + Optional activeDatabase = stateManager.getActiveDatabase(); + + activeDatabase.stream() + .map(BibDatabaseContext::getEntries) + .flatMap(Collection::stream) + .forEach(bibEntry -> bibEntry.getField(StandardField.DOI) + .ifPresent(doi -> + doiCache.put(doi, bibEntry) + )); + } + + public Optional checkDOI(String doiInput) { + if (doiInput == null || doiInput.isBlank()) { + return Optional.empty(); + } + + LayoutFormatter doiStrip = new DOIStrip(); + String normalized = doiStrip.format(doiInput.toLowerCase()); + + if (doiCache.containsKey(normalized)) { + duplicateEntry = doiCache.get(normalized); + return Optional.of(ValidationMessage.warning(Localization.lang("Entry already exists in a library"))); + } + + return Optional.empty(); + } + + public BibEntry getDuplicateEntry() { + return duplicateEntry; + } + public ReadOnlyBooleanProperty executingProperty() { return executing; } @@ -146,6 +194,10 @@ public ReadOnlyBooleanProperty idTextValidatorProperty() { return idTextValidator.getValidationStatus().validProperty(); } + public ValidationStatus duplicateDoiValidatorStatus() { + return duplicateDoiValidator.getValidationStatus(); + } + public ListProperty idFetchersProperty() { return idFetchers; } diff --git a/jabgui/src/main/resources/org/jabref/gui/newentry/NewEntry.fxml b/jabgui/src/main/resources/org/jabref/gui/newentry/NewEntry.fxml index 116ceb1e12c..a9721da734b 100644 --- a/jabgui/src/main/resources/org/jabref/gui/newentry/NewEntry.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/newentry/NewEntry.fxml @@ -4,6 +4,7 @@ + @@ -16,6 +17,7 @@ + @@ -58,11 +60,14 @@ diff --git a/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java b/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java index 24555a5a158..0812b9c021e 100644 --- a/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java +++ b/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java @@ -76,8 +76,8 @@ public DuplicateCheck(BibEntryTypesManager entryTypesManager) { private static boolean haveSameIdentifier(final BibEntry one, final BibEntry two) { return one.getFields().stream() - .filter(field -> field.getProperties().contains(FieldProperty.IDENTIFIER)) - .anyMatch(field -> two.getField(field).map(content -> one.getField(field).orElseThrow().equals(content)).orElse(false)); + .filter(field -> field.getProperties().contains(FieldProperty.IDENTIFIER)) + .anyMatch(field -> two.getField(field).map(content -> one.getField(field).orElseThrow().equals(content)).orElse(false)); } private static boolean haveDifferentEntryType(final BibEntry one, final BibEntry two) { diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 131266ce455..3aabdd08595 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2916,6 +2916,7 @@ Enter\ identifier...=Enter identifier... Enter\ identifier=Enter identifier Enter\ plain\ citations\ to\ parse,\ separated\ by\ blank\ lines.=Enter plain citations to parse, separated by blank lines. Enter\ the\ reference\ identifier\ to\ search\ for.=Enter the reference identifier to search for. +Entry\ already\ exists\ in\ a\ library=Entry already exists in a library Failed\ to\ interpret\ citations.\nThe\ following\ error\ was\ encountered\:\n%0=Failed to interpret citations.\nThe following error was encountered:\n%0 Failed\ to\ interpret\ citations=Failed to interpret citations Failed\ to\ lookup\ identifier=Failed to lookup identifier @@ -2930,6 +2931,7 @@ Interpret\ citations...=Interpret citations... Interpret\ citations=Interpret citations Invalid\ result=Invalid result Invalid\ result\ returned=Invalid result returned +Jump\ to\ entry=Jump to entry New\ Entry=New Entry Other\ types=Other types Parser=Parser