diff --git a/CHANGELOG.md b/CHANGELOG.md index b20c6bf2e94..49785c7fc89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We enabled creating a new file link manually. [#11017](https://github.com/JabRef/jabref/issues/11017) - We added a toggle button to invert the selected groups. [#9073](https://github.com/JabRef/jabref/issues/9073) - We reintroduced the floating search in the main table. [#4237](https://github.com/JabRef/jabref/issues/4237) +- We improved [cleanup](https://docs.jabref.org/finding-sorting-and-cleaning-entries/cleanupentries) of `arXiv` IDs in distributed in the fields `note`, `version`, `institution`, and `eid` fields. [#11306](https://github.com/JabRef/jabref/issues/11306) - We added a switch not to store the linked file URL, because it caused troubles at other apps. [#11735](https://github.com/JabRef/jabref/pull/11735) - When starting a new SLR, the selected catalogs now persist within and across JabRef sessions. [koppor#614](https://github.com/koppor/jabref/issues/614) - We added a different background color to the search bar to indicate when the search syntax is wrong. [#11658](https://github.com/JabRef/jabref/pull/11658) diff --git a/PRIVACY.md b/PRIVACY.md index 853f5371530..d21c0fbbf04 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -38,45 +38,46 @@ These third parties may log additional information besides your IP address and t These third-party services are the following: -| Service | Privacy Policy | -|------------------------------------------------------------------------------------------------------------------------------|----------------| -| [ACM](https://www.acm.org/) | | -| [ACS Publications](https://pubs.acs.org/) | | -| [APS Advancing Physics](https://harvest.aps.org/) | | -| [arXiv.org](https://arxiv.org/) | | -| [Bibliotheksverbund Bayern](https://www.bib-bvb.de/) | | -| [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/) | | -| [Collection of Computer Science Bibliographies](https://en.wikipedia.org/wiki/Collection_of_Computer_Science_Bibliographies) | **currently unavailable**, offline | -| [CrossRef](https://www.crossref.org/) | | -| [dblp](https://dblp.uni-trier.de/) | | -| [DJL (Deep Java Library)](https://djl.ai/) | | -| [Directory of Open Access Books](https://www.doabooks.org/) | | -| [Digitala Vetenskapliga Arkivet](https://www.diva-portal.org/) | | -| [DOI Foundation](https://www.doi.org/) | | -| [Elsevier](https://www.elsevier.com/) | | -| [Google Scholar](https://scholar.google.com/) | | -| [Gemeinsamer Verbundkatalog](https://www.gbv.de/) | | -| [Hugging Face](https://huggingface.co/) | | -| [IACR](https://www.iacr.org/) | | -| [IEEEXplore](https://ieeexplore.ieee.org/Xplore/home.jsp) | | +| Service | Privacy Policy | +|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [ACM](https://www.acm.org/) | | +| [ACS Publications](https://pubs.acs.org/) | | +| [APS Advancing Physics](https://harvest.aps.org/) | | +| [arXiv.org](https://arxiv.org/) | | +| [Bibliotheksverbund Bayern](https://www.bib-bvb.de/) | | +| [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/) | | +| [Collection of Computer Science Bibliographies](https://en.wikipedia.org/wiki/Collection_of_Computer_Science_Bibliographies) | **currently unavailable**, offline | +| [CrossRef](https://www.crossref.org/) | | +| [dblp](https://dblp.uni-trier.de/) | | +| [DJL (Deep Java Library)](https://djl.ai/) | | +| [Directory of Open Access Books](https://www.doabooks.org/) | | +| [Digitala Vetenskapliga Arkivet](https://www.diva-portal.org/) | | +| [DOI Foundation](https://www.doi.org/) | | +| [Elsevier](https://www.elsevier.com/) | | +| [Google Gemini](https://ai.google.dev/gemini-api) | | +| [Google Scholar](https://scholar.google.com/) | | +| [Gemeinsamer Verbundkatalog](https://www.gbv.de/) | | +| [Hugging Face](https://huggingface.co/) | | +| [IACR](https://www.iacr.org/) | | +| [IEEEXplore](https://ieeexplore.ieee.org/Xplore/home.jsp) | | | [INSPIRE](https://inspirehep.net/) | | -| [ISIDORE](https://isidore.science/) | | -| [JSTOR](https://www.jstor.org/) | | -| [Library of Congress](https://lccn.loc.gov/) | | -| [Mistral AI](https://mistral.ai/) | | -| [National Library of Medicine](https://www.ncbi.nlm.nih.gov/) | | -| [MathSciNet](http://www.ams.org/mathscinet) | | -| [mEDRA](https://www.medra.org/) | | -| [Mr. DLib](https://mr-dlib.org/) [1] | | -| [OpenAI](https://openai.com/) | | -| [Openlibrary](https://openlibrary.org) | | -| [ResearchGate](https://www.researchgate.net/) | | -| [IETF Datatracker](https://datatracker.ietf.org/) | | -| [Semantic Scholar](https://www.semanticscholar.org/), powered by [Allen Institute for AI](https://allenai.org/) | | -| [Springer Nature](https://dev.springernature.com/) | | -| [The SAO/NASA Astrophysics Data System](https://ui.adsabs.harvard.edu/) | | -| [Unpaywall](https://unpaywall.org/) | | -| [zbMATH Open](https://www.zbmath.org) | | +| [ISIDORE](https://isidore.science/) | | +| [JSTOR](https://www.jstor.org/) | | +| [Library of Congress](https://lccn.loc.gov/) | | +| [Mistral AI](https://mistral.ai/) | | +| [National Library of Medicine](https://www.ncbi.nlm.nih.gov/) | | +| [MathSciNet](http://www.ams.org/mathscinet) | | +| [mEDRA](https://www.medra.org/) | | +| [Mr. DLib](https://mr-dlib.org/) [1] | | +| [OpenAI](https://openai.com/) | | +| [Openlibrary](https://openlibrary.org) | | +| [ResearchGate](https://www.researchgate.net/) | | +| [IETF Datatracker](https://datatracker.ietf.org/) | | +| [Semantic Scholar](https://www.semanticscholar.org/), powered by [Allen Institute for AI](https://allenai.org/) | | +| [Springer Nature](https://dev.springernature.com/) | | +| [The SAO/NASA Astrophysics Data System](https://ui.adsabs.harvard.edu/) | | +| [Unpaywall](https://unpaywall.org/) | | +| [zbMATH Open](https://www.zbmath.org) | | [1]: *Note: The Mr. DLib service is used for the related articles tab in the entry editor and collects also your language, your browser and operating system (*disabled* by default).* diff --git a/build.gradle b/build.gradle index 94a14221c10..abcd0eec189 100644 --- a/build.gradle +++ b/build.gradle @@ -328,12 +328,13 @@ dependencies { implementation 'org.yaml:snakeyaml:2.3' // AI - implementation 'dev.langchain4j:langchain4j:0.33.0' + implementation 'dev.langchain4j:langchain4j:0.34.0' // Even though we use jvm-openai for LLM connection, we still need this package for tokenization. - implementation('dev.langchain4j:langchain4j-open-ai:0.33.0') { + implementation('dev.langchain4j:langchain4j-open-ai:0.34.0') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' } - implementation('dev.langchain4j:langchain4j-mistral-ai:0.33.0') + implementation('dev.langchain4j:langchain4j-mistral-ai:0.34.0') + implementation('dev.langchain4j:langchain4j-google-ai-gemini:0.34.0') implementation('dev.langchain4j:langchain4j-hugging-face:0.34.0') implementation 'ai.djl:api:0.29.0' implementation 'ai.djl.pytorch:pytorch-model-zoo:0.29.0' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 65e89455413..0ef0ae1ea6e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -183,5 +183,6 @@ requires mslinks; requires org.antlr.antlr4.runtime; requires org.libreoffice.uno; + requires langchain4j.google.ai.gemini; // endregion } diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml index dac1c1ab657..2f439beac31 100644 --- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml @@ -32,7 +32,7 @@ - + @@ -41,16 +41,25 @@ - + + + + + + + + + + - + diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java index 240793a6cce..9681379d60d 100644 --- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java @@ -11,7 +11,9 @@ import org.jabref.gui.DialogService; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.logic.ai.AiDefaultPreferences; import org.jabref.logic.ai.AiPreferences; +import org.jabref.model.ai.AiProvider; import com.airhacks.afterburner.views.ViewLoader; import org.slf4j.Logger; @@ -22,6 +24,7 @@ public class PrivacyNoticeComponent extends ScrollPane { @FXML private TextFlow openAiPrivacyTextFlow; @FXML private TextFlow mistralAiPrivacyTextFlow; + @FXML private TextFlow geminiPrivacyTextFlow; @FXML private TextFlow huggingFacePrivacyTextFlow; @FXML private Text embeddingModelText; @@ -43,9 +46,10 @@ public PrivacyNoticeComponent(AiPreferences aiPreferences, Runnable onIAgreeButt @FXML private void initialize() { - initPrivacyHyperlink(openAiPrivacyTextFlow, "https://openai.com/policies/privacy-policy/"); - initPrivacyHyperlink(mistralAiPrivacyTextFlow, "https://mistral.ai/terms/#privacy-policy"); - initPrivacyHyperlink(huggingFacePrivacyTextFlow, "https://huggingface.co/privacy"); + initPrivacyHyperlink(openAiPrivacyTextFlow, AiProvider.OPEN_AI); + initPrivacyHyperlink(mistralAiPrivacyTextFlow, AiProvider.MISTRAL_AI); + initPrivacyHyperlink(geminiPrivacyTextFlow, AiProvider.GEMINI); + initPrivacyHyperlink(huggingFacePrivacyTextFlow, AiProvider.HUGGING_FACE); String newEmbeddingModelText = embeddingModelText.getText().replaceAll("%0", aiPreferences.getEmbeddingModel().sizeInfo()); embeddingModelText.setText(newEmbeddingModelText); @@ -56,20 +60,19 @@ private void initialize() { embeddingModelText.wrappingWidthProperty().bind(this.widthProperty()); } - private void initPrivacyHyperlink(TextFlow textFlow, String link) { + private void initPrivacyHyperlink(TextFlow textFlow, AiProvider aiProvider) { if (textFlow.getChildren().isEmpty() || !(textFlow.getChildren().getFirst() instanceof Text text)) { return; } - String[] stringArray = text.getText().split("%0"); + String replacedText = text.getText().replaceAll("%0", aiProvider.getLabel()).replace("%1", ""); - if (stringArray.length != 2) { - return; - } + replacedText = replacedText.endsWith(".") ? replacedText.substring(0, replacedText.length() - 1) : replacedText; + text.setText(replacedText); text.wrappingWidthProperty().bind(this.widthProperty()); - text.setText(stringArray[0]); + String link = AiDefaultPreferences.PROVIDERS_PRIVACY_POLICIES.get(aiProvider); Hyperlink hyperlink = new Hyperlink(link); hyperlink.setWrapText(true); hyperlink.setFont(text.getFont()); @@ -79,11 +82,11 @@ private void initPrivacyHyperlink(TextFlow textFlow, String link) { textFlow.getChildren().add(hyperlink); - Text postText = new Text(stringArray[1]); - postText.setFont(text.getFont()); - postText.wrappingWidthProperty().bind(this.widthProperty()); + Text dot = new Text("."); + dot.setFont(text.getFont()); + dot.wrappingWidthProperty().bind(this.widthProperty()); - textFlow.getChildren().add(postText); + textFlow.getChildren().add(dot); } @FXML diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java index 9249a46a1b6..8ba7c3a66c0 100644 --- a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java @@ -49,12 +49,14 @@ public class AiTabViewModel implements PreferenceTabViewModel { private final StringProperty openAiChatModel = new SimpleStringProperty(); private final StringProperty mistralAiChatModel = new SimpleStringProperty(); + private final StringProperty geminiChatModel = new SimpleStringProperty(); private final StringProperty huggingFaceChatModel = new SimpleStringProperty(); private final StringProperty currentApiKey = new SimpleStringProperty(); private final StringProperty openAiApiKey = new SimpleStringProperty(); private final StringProperty mistralAiApiKey = new SimpleStringProperty(); + private final StringProperty geminiAiApiKey = new SimpleStringProperty(); private final StringProperty huggingFaceApiKey = new SimpleStringProperty(); private final BooleanProperty customizeExpertSettings = new SimpleBooleanProperty(); @@ -64,10 +66,11 @@ public class AiTabViewModel implements PreferenceTabViewModel { private final ObjectProperty selectedEmbeddingModel = new SimpleObjectProperty<>(); private final StringProperty currentApiBaseUrl = new SimpleStringProperty(); - private final BooleanProperty disableApiBaseUrl = new SimpleBooleanProperty(true); // {@link HuggingFaceChatModel} doesn't support setting API base URL + private final BooleanProperty disableApiBaseUrl = new SimpleBooleanProperty(true); // {@link HuggingFaceChatModel} and {@link GoogleAiGeminiChatModel} doesn't support setting API base URL private final StringProperty openAiApiBaseUrl = new SimpleStringProperty(); private final StringProperty mistralAiApiBaseUrl = new SimpleStringProperty(); + private final StringProperty geminiApiBaseUrl = new SimpleStringProperty(); private final StringProperty huggingFaceApiBaseUrl = new SimpleStringProperty(); private final StringProperty instruction = new SimpleStringProperty(); @@ -120,7 +123,7 @@ public AiTabViewModel(CliPreferences preferences) { String oldChatModel = currentChatModel.get(); chatModelsList.setAll(models); - disableApiBaseUrl.set(newValue == AiProvider.HUGGING_FACE); + disableApiBaseUrl.set(newValue == AiProvider.HUGGING_FACE || newValue == AiProvider.GEMINI); if (oldValue != null) { switch (oldValue) { @@ -134,6 +137,11 @@ public AiTabViewModel(CliPreferences preferences) { mistralAiApiKey.set(currentApiKey.get()); mistralAiApiBaseUrl.set(currentApiBaseUrl.get()); } + case GEMINI -> { + geminiChatModel.set(oldChatModel); + geminiAiApiKey.set(currentApiKey.get()); + geminiApiBaseUrl.set(currentApiBaseUrl.get()); + } case HUGGING_FACE -> { huggingFaceChatModel.set(oldChatModel); huggingFaceApiKey.set(currentApiKey.get()); @@ -153,6 +161,11 @@ public AiTabViewModel(CliPreferences preferences) { currentApiKey.set(mistralAiApiKey.get()); currentApiBaseUrl.set(mistralAiApiBaseUrl.get()); } + case GEMINI -> { + currentChatModel.set(geminiChatModel.get()); + currentApiKey.set(geminiAiApiKey.get()); + currentApiBaseUrl.set(geminiApiBaseUrl.get()); + } case HUGGING_FACE -> { currentChatModel.set(huggingFaceChatModel.get()); currentApiKey.set(huggingFaceApiKey.get()); @@ -165,6 +178,7 @@ public AiTabViewModel(CliPreferences preferences) { switch (selectedAiProvider.get()) { case OPEN_AI -> openAiChatModel.set(newValue); case MISTRAL_AI -> mistralAiChatModel.set(newValue); + case GEMINI -> geminiChatModel.set(newValue); case HUGGING_FACE -> huggingFaceChatModel.set(newValue); } @@ -182,6 +196,7 @@ public AiTabViewModel(CliPreferences preferences) { switch (selectedAiProvider.get()) { case OPEN_AI -> openAiApiKey.set(newValue); case MISTRAL_AI -> mistralAiApiKey.set(newValue); + case GEMINI -> geminiAiApiKey.set(newValue); case HUGGING_FACE -> huggingFaceApiKey.set(newValue); } }); @@ -190,6 +205,7 @@ public AiTabViewModel(CliPreferences preferences) { switch (selectedAiProvider.get()) { case OPEN_AI -> openAiApiBaseUrl.set(newValue); case MISTRAL_AI -> mistralAiApiBaseUrl.set(newValue); + case GEMINI -> geminiApiBaseUrl.set(newValue); case HUGGING_FACE -> huggingFaceApiBaseUrl.set(newValue); } }); @@ -265,14 +281,17 @@ public AiTabViewModel(CliPreferences preferences) { public void setValues() { openAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.OPEN_AI)); mistralAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.MISTRAL_AI)); + geminiAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.GEMINI)); huggingFaceApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.HUGGING_FACE)); openAiApiBaseUrl.setValue(aiPreferences.getOpenAiApiBaseUrl()); mistralAiApiBaseUrl.setValue(aiPreferences.getMistralAiApiBaseUrl()); + geminiApiBaseUrl.setValue(aiPreferences.getGeminiApiBaseUrl()); huggingFaceApiBaseUrl.setValue(aiPreferences.getHuggingFaceApiBaseUrl()); openAiChatModel.setValue(aiPreferences.getOpenAiChatModel()); mistralAiChatModel.setValue(aiPreferences.getMistralAiChatModel()); + geminiChatModel.setValue(aiPreferences.getGeminiChatModel()); huggingFaceChatModel.setValue(aiPreferences.getHuggingFaceChatModel()); enableAi.setValue(aiPreferences.getEnableAi()); @@ -282,7 +301,6 @@ public void setValues() { customizeExpertSettings.setValue(aiPreferences.getCustomizeExpertSettings()); selectedEmbeddingModel.setValue(aiPreferences.getEmbeddingModel()); - instruction.setValue(aiPreferences.getInstruction()); temperature.setValue(LocalizedNumbers.doubleToString(aiPreferences.getTemperature())); contextWindowSize.setValue(aiPreferences.getContextWindowSize()); @@ -300,10 +318,12 @@ public void storeSettings() { aiPreferences.setOpenAiChatModel(openAiChatModel.get() == null ? "" : openAiChatModel.get()); aiPreferences.setMistralAiChatModel(mistralAiChatModel.get() == null ? "" : mistralAiChatModel.get()); + aiPreferences.setGeminiChatModel(geminiChatModel.get() == null ? "" : geminiChatModel.get()); aiPreferences.setHuggingFaceChatModel(huggingFaceChatModel.get() == null ? "" : huggingFaceChatModel.get()); aiPreferences.storeAiApiKeyInKeyring(AiProvider.OPEN_AI, openAiApiKey.get() == null ? "" : openAiApiKey.get()); aiPreferences.storeAiApiKeyInKeyring(AiProvider.MISTRAL_AI, mistralAiApiKey.get() == null ? "" : mistralAiApiKey.get()); + aiPreferences.storeAiApiKeyInKeyring(AiProvider.GEMINI, geminiAiApiKey.get() == null ? "" : geminiAiApiKey.get()); aiPreferences.storeAiApiKeyInKeyring(AiProvider.HUGGING_FACE, huggingFaceApiKey.get() == null ? "" : huggingFaceApiKey.get()); // We notify in all cases without a real check if something was changed aiPreferences.apiKeyUpdated(); @@ -314,6 +334,7 @@ public void storeSettings() { aiPreferences.setOpenAiApiBaseUrl(openAiApiBaseUrl.get() == null ? "" : openAiApiBaseUrl.get()); aiPreferences.setMistralAiApiBaseUrl(mistralAiApiBaseUrl.get() == null ? "" : mistralAiApiBaseUrl.get()); + aiPreferences.setGeminiApiBaseUrl(geminiApiBaseUrl.get() == null ? "" : geminiApiBaseUrl.get()); aiPreferences.setHuggingFaceApiBaseUrl(huggingFaceApiBaseUrl.get() == null ? "" : huggingFaceApiBaseUrl.get()); aiPreferences.setInstruction(instruction.get()); diff --git a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java index 0beb26c3912..b17fa670867 100644 --- a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java +++ b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java @@ -11,12 +11,21 @@ public class AiDefaultPreferences { AiProvider.OPEN_AI, List.of("gpt-4o-mini", "gpt-4o", "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"), // "mistral" and "mixtral" are not language mistakes. AiProvider.MISTRAL_AI, List.of("open-mistral-nemo", "open-mistral-7b", "open-mixtral-8x7b", "open-mixtral-8x22b", "mistral-large-latest"), + AiProvider.GEMINI, List.of("gemini-1.5-flash", "gemini-1.5-pro", "gemini-1.0-pro"), AiProvider.HUGGING_FACE, List.of() ); + public static final Map PROVIDERS_PRIVACY_POLICIES = Map.of( + AiProvider.OPEN_AI, "https://openai.com/policies/privacy-policy/", + AiProvider.MISTRAL_AI, "https://mistral.ai/terms/#privacy-policy", + AiProvider.GEMINI, "https://ai.google.dev/gemini-api/terms", + AiProvider.HUGGING_FACE, "https://huggingface.co/privacy" + ); + public static final Map PROVIDERS_API_URLS = Map.of( AiProvider.OPEN_AI, "https://api.openai.com/v1", AiProvider.MISTRAL_AI, "https://api.mistral.ai/v1", + AiProvider.GEMINI, "https://generativelanguage.googleapis.com/v1beta/", AiProvider.HUGGING_FACE, "https://huggingface.co/api" ); @@ -34,6 +43,11 @@ public class AiDefaultPreferences { "open-mistral-7b", 32000, "open-mixtral-8x7b", 32000, "open-mixtral-8x22b", 64000 + ), + AiProvider.GEMINI, Map.of( + "gemini-1.5-flash", 1048576, + "gemini-1.5-pro", 2097152, + "gemini-1.0-pro", 32000 ) ); @@ -44,6 +58,7 @@ public class AiDefaultPreferences { public static final Map CHAT_MODELS = Map.of( AiProvider.OPEN_AI, "gpt-4o-mini", AiProvider.MISTRAL_AI, "open-mixtral-8x22b", + AiProvider.GEMINI, "gemini-1.5-flash", AiProvider.HUGGING_FACE, "" ); diff --git a/src/main/java/org/jabref/logic/ai/AiPreferences.java b/src/main/java/org/jabref/logic/ai/AiPreferences.java index 15671fbae61..d3c8bb345ae 100644 --- a/src/main/java/org/jabref/logic/ai/AiPreferences.java +++ b/src/main/java/org/jabref/logic/ai/AiPreferences.java @@ -36,12 +36,14 @@ public class AiPreferences { private final StringProperty openAiChatModel; private final StringProperty mistralAiChatModel; + private final StringProperty geminiChatModel; private final StringProperty huggingFaceChatModel; private final BooleanProperty customizeExpertSettings; private final StringProperty openAiApiBaseUrl; private final StringProperty mistralAiApiBaseUrl; + private final StringProperty geminiApiBaseUrl; private final StringProperty huggingFaceApiBaseUrl; private final ObjectProperty embeddingModel; @@ -59,10 +61,12 @@ public AiPreferences(boolean enableAi, AiProvider aiProvider, String openAiChatModel, String mistralAiChatModel, + String geminiChatModel, String huggingFaceChatModel, boolean customizeExpertSettings, String openAiApiBaseUrl, String mistralAiApiBaseUrl, + String geminiApiBaseUrl, String huggingFaceApiBaseUrl, EmbeddingModel embeddingModel, String instruction, @@ -79,12 +83,14 @@ public AiPreferences(boolean enableAi, this.openAiChatModel = new SimpleStringProperty(openAiChatModel); this.mistralAiChatModel = new SimpleStringProperty(mistralAiChatModel); + this.geminiChatModel = new SimpleStringProperty(geminiChatModel); this.huggingFaceChatModel = new SimpleStringProperty(huggingFaceChatModel); this.customizeExpertSettings = new SimpleBooleanProperty(customizeExpertSettings); this.openAiApiBaseUrl = new SimpleStringProperty(openAiApiBaseUrl); this.mistralAiApiBaseUrl = new SimpleStringProperty(mistralAiApiBaseUrl); + this.geminiApiBaseUrl = new SimpleStringProperty(geminiApiBaseUrl); this.huggingFaceApiBaseUrl = new SimpleStringProperty(huggingFaceApiBaseUrl); this.embeddingModel = new SimpleObjectProperty<>(embeddingModel); @@ -100,8 +106,7 @@ public AiPreferences(boolean enableAi, public String getApiKeyForAiProvider(AiProvider aiProvider) { try (final Keyring keyring = Keyring.create()) { return keyring.getPassword(KEYRING_AI_SERVICE, KEYRING_AI_SERVICE_ACCOUNT + "-" + aiProvider.name()); - } catch ( - PasswordAccessException e) { + } catch (PasswordAccessException e) { LOGGER.debug("No API key stored for provider {}. Returning an empty string", aiProvider.getLabel()); return ""; } catch (Exception e) { @@ -174,6 +179,18 @@ public void setMistralAiChatModel(String mistralAiChatModel) { this.mistralAiChatModel.set(mistralAiChatModel); } + public StringProperty geminiChatModelProperty() { + return geminiChatModel; + } + + public String getGeminiChatModel() { + return geminiChatModel.get(); + } + + public void setGeminiChatModel(String geminiChatModel) { + this.geminiChatModel.set(geminiChatModel); + } + public StringProperty huggingFaceChatModelProperty() { return huggingFaceChatModel; } @@ -238,6 +255,18 @@ public void setMistralAiApiBaseUrl(String mistralAiApiBaseUrl) { this.mistralAiApiBaseUrl.set(mistralAiApiBaseUrl); } + public StringProperty geminiApiBaseUrlProperty() { + return geminiApiBaseUrl; + } + + public String getGeminiApiBaseUrl() { + return geminiApiBaseUrl.get(); + } + + public void setGeminiApiBaseUrl(String geminiApiBaseUrl) { + this.geminiApiBaseUrl.set(geminiApiBaseUrl); + } + public StringProperty huggingFaceApiBaseUrlProperty() { return huggingFaceApiBaseUrl; } @@ -294,6 +323,7 @@ public int getContextWindowSize() { case OPEN_AI -> AiDefaultPreferences.getContextWindowSize(AiProvider.OPEN_AI, openAiChatModel.get()); case MISTRAL_AI -> AiDefaultPreferences.getContextWindowSize(AiProvider.MISTRAL_AI, mistralAiChatModel.get()); case HUGGING_FACE -> AiDefaultPreferences.getContextWindowSize(AiProvider.HUGGING_FACE, huggingFaceChatModel.get()); + case GEMINI -> AiDefaultPreferences.getContextWindowSize(AiProvider.GEMINI, geminiChatModel.get()); }; } } @@ -419,6 +449,8 @@ public String getSelectedChatModel() { mistralAiChatModel.get(); case HUGGING_FACE -> huggingFaceChatModel.get(); + case GEMINI -> + geminiChatModel.get(); }; } @@ -431,6 +463,8 @@ public String getSelectedApiBaseUrl() { mistralAiApiBaseUrl.get(); case HUGGING_FACE -> huggingFaceApiBaseUrl.get(); + case GEMINI -> + geminiApiBaseUrl.get(); }; } else { return AiDefaultPreferences.PROVIDERS_API_URLS.get(aiProvider.get()); diff --git a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java index 765c395f146..92fc11df484 100644 --- a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java +++ b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java @@ -16,6 +16,7 @@ import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.huggingface.HuggingFaceChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.output.Response; @@ -76,8 +77,20 @@ private void rebuild() { ); } + case GEMINI -> { + // NOTE: {@link GoogleAiGeminiChatModel} doesn't support API base url. + langchainChatModel = Optional.of(GoogleAiGeminiChatModel + .builder() + .apiKey(apiKey) + .modelName(aiPreferences.getSelectedChatModel()) + .temperature(aiPreferences.getTemperature()) + .logRequestsAndResponses(true) + .build() + ); + } + case HUGGING_FACE -> { - // NOTE: {@link HuggingFaceChatModel} doesn't support API base url :( + // NOTE: {@link HuggingFaceChatModel} doesn't support API base url. langchainChatModel = Optional.of(HuggingFaceChatModel .builder() .accessToken(apiKey) diff --git a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java index 2407cf0fe7f..0447f748cef 100644 --- a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java +++ b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java @@ -36,6 +36,7 @@ import org.jabref.model.strings.LatexToUnicodeAdapter; import org.jabref.model.strings.StringUtil; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1218,14 +1219,14 @@ protected static List parseFieldAndModifiers(String arg) { *
  • null if content is null
  • * */ - private static String generateInstitutionKey(String content) { + @VisibleForTesting + static String generateInstitutionKey(String content) { if (content == null) { return null; } if (content.isBlank()) { return ""; } - Matcher matcher = INLINE_ABBREVIATION.matcher(content); if (matcher.find()) { return LatexToUnicodeAdapter.format(matcher.group()); @@ -1309,8 +1310,7 @@ private static String generateInstitutionKey(String content) { // Putting parts together. return (university == null ? Objects.toString(rest, "") : university) + (school == null ? "" : school) - + ((department == null) - || ((school != null) && department.equals(school)) ? "" : department); + + ((department == null) || (department.equals(school)) ? "" : department); } /** diff --git a/src/main/java/org/jabref/logic/cleanup/EprintCleanup.java b/src/main/java/org/jabref/logic/cleanup/EprintCleanup.java index 27ab856c499..abc56b9db6a 100644 --- a/src/main/java/org/jabref/logic/cleanup/EprintCleanup.java +++ b/src/main/java/org/jabref/logic/cleanup/EprintCleanup.java @@ -13,6 +13,8 @@ /** * Formats the DOI (e.g. removes http part) and also moves DOIs from note, url or ee field to the doi field. + * + * Background information on tex.stackexchange. */ public class EprintCleanup implements CleanupJob { @@ -20,11 +22,25 @@ public class EprintCleanup implements CleanupJob { public List cleanup(BibEntry entry) { List changes = new ArrayList<>(); - for (Field field : Arrays.asList(StandardField.URL, StandardField.JOURNAL, StandardField.JOURNALTITLE, StandardField.NOTE)) { + Optional version = entry.getField(StandardField.VERSION); + Optional institution = entry.getField(StandardField.INSTITUTION); + + for (Field field : Arrays.asList(StandardField.URL, StandardField.JOURNAL, StandardField.JOURNALTITLE, StandardField.NOTE, StandardField.EID)) { Optional arXivIdentifier = entry.getField(field).flatMap(ArXivIdentifier::parse); if (arXivIdentifier.isPresent()) { - entry.setField(StandardField.EPRINT, arXivIdentifier.get().getNormalized()) + String normalizedEprint = arXivIdentifier.get().getNormalized(); + + if (version.isPresent() && !normalizedEprint.contains("v" + version.get())) { + normalizedEprint += "v" + version.get(); + } + + if (institution.isPresent() && "arxiv".equalsIgnoreCase(institution.get())) { + entry.clearField(StandardField.INSTITUTION) + .ifPresent(changes::add); + } + + entry.setField(StandardField.EPRINT, normalizedEprint) .ifPresent(changes::add); entry.setField(StandardField.EPRINTTYPE, "arxiv") @@ -45,6 +61,7 @@ public List cleanup(BibEntry entry) { } } } + entry.clearField(StandardField.VERSION).ifPresent(changes::add); return changes; } diff --git a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index e1326f0a89e..99d6441eec5 100644 --- a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -334,11 +334,13 @@ public class JabRefCliPreferences implements CliPreferences { private static final String AI_PROVIDER = "aiProvider"; private static final String AI_OPEN_AI_CHAT_MODEL = "aiOpenAiChatModel"; private static final String AI_MISTRAL_AI_CHAT_MODEL = "aiMistralAiChatModel"; + private static final String AI_GEMINI_CHAT_MODEL = "aiGeminiChatModel"; private static final String AI_HUGGING_FACE_CHAT_MODEL = "aiHuggingFaceChatModel"; private static final String AI_CUSTOMIZE_SETTINGS = "aiCustomizeSettings"; private static final String AI_EMBEDDING_MODEL = "aiEmbeddingModel"; private static final String AI_OPEN_AI_API_BASE_URL = "aiOpenAiApiBaseUrl"; private static final String AI_MISTRAL_AI_API_BASE_URL = "aiMistralAiApiBaseUrl"; + private static final String AI_GEMINI_API_BASE_URL = "aiGeminiApiBaseUrl"; private static final String AI_HUGGING_FACE_API_BASE_URL = "aiHuggingFaceApiBaseUrl"; private static final String AI_SYSTEM_MESSAGE = "aiSystemMessage"; private static final String AI_TEMPERATURE = "aiTemperature"; @@ -604,11 +606,13 @@ protected JabRefCliPreferences() { defaults.put(AI_PROVIDER, AiDefaultPreferences.PROVIDER.name()); defaults.put(AI_OPEN_AI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.OPEN_AI)); defaults.put(AI_MISTRAL_AI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.MISTRAL_AI)); + defaults.put(AI_GEMINI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.GEMINI)); defaults.put(AI_HUGGING_FACE_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.HUGGING_FACE)); defaults.put(AI_CUSTOMIZE_SETTINGS, AiDefaultPreferences.CUSTOMIZE_SETTINGS); defaults.put(AI_EMBEDDING_MODEL, AiDefaultPreferences.EMBEDDING_MODEL.name()); defaults.put(AI_OPEN_AI_API_BASE_URL, AiDefaultPreferences.PROVIDERS_API_URLS.get(AiProvider.OPEN_AI)); defaults.put(AI_MISTRAL_AI_API_BASE_URL, AiDefaultPreferences.PROVIDERS_API_URLS.get(AiProvider.MISTRAL_AI)); + defaults.put(AI_GEMINI_API_BASE_URL, AiDefaultPreferences.PROVIDERS_API_URLS.get(AiProvider.GEMINI)); defaults.put(AI_HUGGING_FACE_API_BASE_URL, AiDefaultPreferences.PROVIDERS_API_URLS.get(AiProvider.HUGGING_FACE)); defaults.put(AI_SYSTEM_MESSAGE, AiDefaultPreferences.SYSTEM_MESSAGE); defaults.put(AI_TEMPERATURE, AiDefaultPreferences.TEMPERATURE); @@ -1737,10 +1741,12 @@ public AiPreferences getAiPreferences() { AiProvider.valueOf(get(AI_PROVIDER)), get(AI_OPEN_AI_CHAT_MODEL), get(AI_MISTRAL_AI_CHAT_MODEL), + get(AI_GEMINI_CHAT_MODEL), get(AI_HUGGING_FACE_CHAT_MODEL), getBoolean(AI_CUSTOMIZE_SETTINGS), get(AI_OPEN_AI_API_BASE_URL), get(AI_MISTRAL_AI_API_BASE_URL), + get(AI_GEMINI_API_BASE_URL), get(AI_HUGGING_FACE_API_BASE_URL), EmbeddingModel.valueOf(get(AI_EMBEDDING_MODEL)), get(AI_SYSTEM_MESSAGE), @@ -1757,12 +1763,14 @@ public AiPreferences getAiPreferences() { EasyBind.listen(aiPreferences.openAiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_OPEN_AI_CHAT_MODEL, newValue)); EasyBind.listen(aiPreferences.mistralAiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_CHAT_MODEL, newValue)); + EasyBind.listen(aiPreferences.geminiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_CHAT_MODEL, newValue)); EasyBind.listen(aiPreferences.huggingFaceChatModelProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_CHAT_MODEL, newValue)); EasyBind.listen(aiPreferences.customizeExpertSettingsProperty(), (obs, oldValue, newValue) -> putBoolean(AI_CUSTOMIZE_SETTINGS, newValue)); EasyBind.listen(aiPreferences.openAiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_OPEN_AI_API_BASE_URL, newValue)); EasyBind.listen(aiPreferences.mistralAiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_API_BASE_URL, newValue)); + EasyBind.listen(aiPreferences.geminiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_API_BASE_URL, newValue)); EasyBind.listen(aiPreferences.huggingFaceApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_API_BASE_URL, newValue)); EasyBind.listen(aiPreferences.embeddingModelProperty(), (obs, oldValue, newValue) -> put(AI_EMBEDDING_MODEL, newValue.name())); diff --git a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java index b3e79e9f261..09f15d48207 100644 --- a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java +++ b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java @@ -37,6 +37,7 @@ public List getMatches() { if (!query.isValid()) { LOGGER.warn("Search failed: invalid search expression"); + luceneManager.closeAndWait(); return Collections.emptyList(); } List matchEntries = luceneManager.search(query) @@ -44,7 +45,7 @@ public List getMatches() { .stream() .map(entryId -> databaseContext.getDatabase().getEntryById(entryId)) .toList(); - luceneManager.close(); + luceneManager.closeAndWait(); return BibDatabases.purgeEmptyEntries(matchEntries); } } diff --git a/src/main/java/org/jabref/logic/search/LuceneIndexer.java b/src/main/java/org/jabref/logic/search/LuceneIndexer.java index 1aaaaaafbc5..4cc8b48763e 100644 --- a/src/main/java/org/jabref/logic/search/LuceneIndexer.java +++ b/src/main/java/org/jabref/logic/search/LuceneIndexer.java @@ -23,4 +23,6 @@ public interface LuceneIndexer { SearcherManager getSearcherManager(); void close(); + + void closeAndWait(); } diff --git a/src/main/java/org/jabref/logic/search/LuceneManager.java b/src/main/java/org/jabref/logic/search/LuceneManager.java index bfb5cd42244..2d0c56f2fcd 100644 --- a/src/main/java/org/jabref/logic/search/LuceneManager.java +++ b/src/main/java/org/jabref/logic/search/LuceneManager.java @@ -209,6 +209,13 @@ public void close() { databaseContext.getDatabase().postEvent(new IndexClosedEvent()); } + public void closeAndWait() { + bibFieldsIndexer.closeAndWait(); + shouldIndexLinkedFiles.removeListener(preferencesListener); + linkedFilesIndexer.closeAndWait(); + databaseContext.getDatabase().postEvent(new IndexClosedEvent()); + } + public AutoCloseable blockLinkedFileIndexer() { LOGGER.debug("Blocking linked files indexer"); isLinkedFilesIndexerBlocked.set(true); diff --git a/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java b/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java index 2c4e191e008..eb0141f3f92 100644 --- a/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java +++ b/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java @@ -156,6 +156,11 @@ public void close() { HeadlessExecutorService.INSTANCE.execute(this::closeIndex); } + @Override + public void closeAndWait() { + HeadlessExecutorService.INSTANCE.executeAndWait(this::closeIndex); + } + private void closeIndex() { try { LOGGER.debug("Closing bib fields index"); diff --git a/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java b/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java index 250f0b8435f..17aec739871 100644 --- a/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java +++ b/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java @@ -315,6 +315,11 @@ public void close() { HeadlessExecutorService.INSTANCE.execute(this::closeIndex); } + @Override + public void closeAndWait() { + HeadlessExecutorService.INSTANCE.executeAndWait(this::closeIndex); + } + private void closeIndex() { try { LOGGER.debug("Closing linked files index"); diff --git a/src/main/java/org/jabref/logic/search/indexing/ReadOnlyLinkedFilesIndexer.java b/src/main/java/org/jabref/logic/search/indexing/ReadOnlyLinkedFilesIndexer.java index 203411eaeff..b12d5c87899 100644 --- a/src/main/java/org/jabref/logic/search/indexing/ReadOnlyLinkedFilesIndexer.java +++ b/src/main/java/org/jabref/logic/search/indexing/ReadOnlyLinkedFilesIndexer.java @@ -63,6 +63,11 @@ public void close() { HeadlessExecutorService.INSTANCE.execute(this::closeIndex); } + @Override + public void closeAndWait() { + HeadlessExecutorService.INSTANCE.executeAndWait(this::closeIndex); + } + private void closeIndex() { try { searcherManager.close(); diff --git a/src/main/java/org/jabref/model/ai/AiProvider.java b/src/main/java/org/jabref/model/ai/AiProvider.java index e93c6ecbfc1..ae33acb97cd 100644 --- a/src/main/java/org/jabref/model/ai/AiProvider.java +++ b/src/main/java/org/jabref/model/ai/AiProvider.java @@ -5,6 +5,7 @@ public enum AiProvider implements Serializable { OPEN_AI("OpenAI"), MISTRAL_AI("Mistral AI"), + GEMINI("Gemini"), HUGGING_FACE("Hugging Face"); private final String label; diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index a60384b6812..80d0c559891 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2570,9 +2570,6 @@ Enable\ AI\ functionality\ (summary\ generation\ and\ chatting)\ in\ JabRef=Enab Customize\ expert\ settings=Customize expert settings These\ parameters\ affect\ how\ the\ AI\ will\ answer\ your\ questions.=These parameters affect how the AI will answer your questions. Chat\ with\ AI\ about\ content\ of\ attached\ file(s)=Chat with AI about content of attached file(s) -If\ you\ have\ chosen\ the\ Hugging\ Face\ as\ AI\ provider,\ the\ privacy\ policy\ of\ Hugging\ Face\ applies.\ You\ find\ it\ at\ %0.=If you have chosen the Hugging Face as AI provider, the privacy policy of Hugging Face applies. You find it at %0. -If\ you\ have\ chosen\ the\ Mistral\ AI\ as\ AI\ provider,\ the\ privacy\ policy\ of\ Mistral\ AI\ applies.\ You\ find\ it\ at\ %0.=If you have chosen the Mistral AI as AI provider, the privacy policy of Mistral AI applies. You find it at %0. -If\ you\ have\ chosen\ the\ OpenAI\ as\ AI\ provider,\ the\ privacy\ policy\ of\ OpenAI\ applies.\ You\ find\ it\ at\ %0.=If you have chosen the OpenAI as AI provider, the privacy policy of OpenAI applies. You find it at %0. In\ order\ to\ use\ AI\ chat,\ you\ need\ to\ enable\ chatting\ with\ attached\ PDF\ files\ in\ JabRef\ preferences\ (AI\ tab).=In order to use AI chat, you need to enable chatting with attached PDF files in JabRef preferences (AI tab). In\ order\ to\ use\ AI\ chat,\ set\ an\ API\ key\ inside\ JabRef\ preferences\ (AI\ tab).=In order to use AI chat, set an API key inside JabRef preferences (AI tab). Unable\ to\ chat\ with\ AI.=Unable to chat with AI. @@ -2632,6 +2629,7 @@ Generating\ embeddings\ for\ %0=Generating embeddings for %0 RAG\ minimum\ score\ must\ be\ a\ number=RAG minimum score must be a number RAG\ minimum\ score\ must\ be\ greater\ than\ 0\ and\ less\ than\ 1=RAG minimum score must be greater than 0 and less than 1 Temperature\ must\ be\ a\ number=Temperature must be a number +If\ you\ have\ chosen\ %0\ as\ an\ AI\ provider,\ the\ privacy\ policy\ of\ %0\ applies.\ You\ find\ it\ at\ %1.=If you have chosen %0 as an AI provider, the privacy policy of %0 applies. You find it at %1. Link=Link Source\ URL=Source URL diff --git a/src/test/java/org/jabref/logic/citationkeypattern/BracketedPatternTest.java b/src/test/java/org/jabref/logic/citationkeypattern/BracketedPatternTest.java index cc466d6b90e..4d1b3649778 100644 --- a/src/test/java/org/jabref/logic/citationkeypattern/BracketedPatternTest.java +++ b/src/test/java/org/jabref/logic/citationkeypattern/BracketedPatternTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; /** @@ -734,4 +735,31 @@ void editorFieldMarkers(String expectedCitationKey, String pattern, String edito BracketedPattern bracketedPattern = new BracketedPattern(pattern); assertEquals(expectedCitationKey, bracketedPattern.expand(bibEntry)); } + + @ParameterizedTest + @CsvSource({ + "'', ''", + "The Attributed Graph Grammar System ({AGG}),AGG", + "'The University of Science',UniScience", + "'School of Business, Department of Management',BM", + "'Graph Systems Research Group',GSRG", + "'The Great Institute, 123 Main Street, Springfield',GreatInstitute", + "'Invalid {\\Unicode}',Invalid", + "'School of Electrical Engineering ({SEE}), Department of Computer Science',SEE", + "'{The Attributed Graph Grammar System ({AGG})}',AGG", + "'{The Attributed Graph Grammar System}',AGGS", + "'{University of Example, Department of Computer Science, Some Address}',UniExampleCS", + "'{Example School of Engineering, Department of Computer Science, Some Address}',SomeAddressEECS", + "'{Example Institute, Computer Science Department, Some Address}',ExampleInstituteCS", + "'{Short Part, Some Address}',ShortPart", + "'{Example with Several Tokens, Some Address}',EST"}) + + void generateInstitutionKeyTest(String input, String expected) { + assertEquals(expected, BracketedPattern.generateInstitutionKey(input)); + } + + @Test + void generateInstitutionKeyNullTest() { + assertNull(BracketedPattern.generateInstitutionKey(null)); + } } diff --git a/src/test/java/org/jabref/logic/cleanup/EprintCleanupTest.java b/src/test/java/org/jabref/logic/cleanup/EprintCleanupTest.java index d27a98e59fa..f391a4bd44f 100644 --- a/src/test/java/org/jabref/logic/cleanup/EprintCleanupTest.java +++ b/src/test/java/org/jabref/logic/cleanup/EprintCleanupTest.java @@ -27,4 +27,41 @@ void cleanupCompleteEntry() { assertEquals(expected, input); } + + @Test + void cleanupEntryWithVersionAndInstitutionAndEid() { + BibEntry input = new BibEntry() + .withField(StandardField.NOTE, "arXiv: 1503.05173") + .withField(StandardField.VERSION, "1") + .withField(StandardField.INSTITUTION, "arXiv") + .withField(StandardField.EID, "arXiv:1503.05173"); + + BibEntry expected = new BibEntry() + .withField(StandardField.EPRINT, "1503.05173v1") + .withField(StandardField.EPRINTTYPE, "arxiv"); + + EprintCleanup cleanup = new EprintCleanup(); + cleanup.cleanup(input); + + assertEquals(expected, input); + } + + @Test + void cleanupEntryWithOtherInstitution() { + BibEntry input = new BibEntry() + .withField(StandardField.NOTE, "arXiv: 1503.05173") + .withField(StandardField.VERSION, "1") + .withField(StandardField.INSTITUTION, "OtherInstitution") + .withField(StandardField.EID, "arXiv:1503.05173"); + + BibEntry expected = new BibEntry() + .withField(StandardField.EPRINT, "1503.05173v1") + .withField(StandardField.EPRINTTYPE, "arxiv") + .withField(StandardField.INSTITUTION, "OtherInstitution"); + + EprintCleanup cleanup = new EprintCleanup(); + cleanup.cleanup(input); + + assertEquals(expected, input); + } } diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java index 3416f578d18..2b2d1eca991 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -1,6 +1,7 @@ package org.jabref.logic.search; import java.io.IOException; +import java.nio.file.Path; import java.util.EnumSet; import java.util.List; import java.util.stream.Stream; @@ -18,24 +19,29 @@ import org.jabref.model.search.SearchQuery; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class DatabaseSearcherTest { private static final TaskExecutor TASK_EXECUTOR = new CurrentThreadTaskExecutor(); private BibDatabaseContext databaseContext; private final FilePreferences filePreferences = mock(FilePreferences.class); + @TempDir + private Path indexDir; @BeforeEach void setUp() { when(filePreferences.shouldFulltextIndexLinkedFiles()).thenReturn(false); when(filePreferences.fulltextIndexLinkedFilesProperty()).thenReturn(mock(BooleanProperty.class)); - databaseContext = new BibDatabaseContext(); + databaseContext = spy(new BibDatabaseContext()); + when(databaseContext.getFulltextIndexPath()).thenReturn(indexDir); } @ParameterizedTest diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java index 79831162174..13e59c1b683 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java @@ -33,6 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; class DatabaseSearcherWithBibFilesTest { @@ -77,7 +78,8 @@ private BibDatabaseContext initializeDatabaseFromPath(String testFile) throws Ex private BibDatabaseContext initializeDatabaseFromPath(Path testFile) throws Exception { ParserResult result = new BibtexImporter(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()).importDatabase(testFile); - BibDatabaseContext databaseContext = result.getDatabaseContext(); + BibDatabaseContext databaseContext = spy(result.getDatabaseContext()); + when(databaseContext.getFulltextIndexPath()).thenReturn(indexDir); when(filePreferences.shouldFulltextIndexLinkedFiles()).thenReturn(true); when(filePreferences.fulltextIndexLinkedFilesProperty()).thenReturn(new SimpleBooleanProperty(true));