From 3ddf7aa4bd39c8e058a2d69652df62ea48005c49 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Tue, 20 May 2025 02:18:22 -0700 Subject: [PATCH 01/11] feat: support searching via google scholar --- .../jabref/gui/actions/StandardActions.java | 1 + .../jabref/gui/maintable/RightClickMenu.java | 1 + .../maintable/SearchGoogleScholarAction.java | 52 +++++++++++++++++++ .../logic/util/ExternalLinkCreator.java | 22 ++++++++ .../main/resources/l10n/JabRef_en.properties | 3 ++ 5 files changed, 79 insertions(+) create mode 100644 jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 647cef5e017..9b9c7046e53 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -37,6 +37,7 @@ public enum StandardActions implements Action { EXTRACT_FILE_REFERENCES_OFFLINE(Localization.lang("Extract references from file (offline)"), IconTheme.JabRefIcons.FILE_STAR), OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), + SEARCH_GOOGLE_SCHOLAR(Localization.lang("Search Google Scholar")), MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/..."), KeyBinding.MERGE_WITH_FETCHED_ENTRY), BATCH_MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0 (fully automated)", "DOI/ISBN/...")), ATTACH_FILE(Localization.lang("Attach file"), IconTheme.JabRefIcons.ATTACH_FILE), diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index 5a9c93e6cac..18185633af2 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -102,6 +102,7 @@ public static ContextMenu create(BibEntryTableViewModel entry, factory.createMenuItem(StandardActions.OPEN_URL, new OpenUrlAction(dialogService, stateManager, preferences)), factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences)), + factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences)), new SeparatorMenuItem(), diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java new file mode 100644 index 00000000000..c135dfb2e31 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java @@ -0,0 +1,52 @@ +package org.jabref.gui.maintable; + +import java.io.IOException; +import java.util.List; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry; +import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.ExternalLinkCreator; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import javafx.beans.binding.BooleanExpression; + +public class SearchGoogleScholarAction extends SimpleCommand { + private final DialogService dialogService; + private final StateManager stateManager; + private final GuiPreferences preferences; + + public SearchGoogleScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) { + this.dialogService = dialogService; + this.stateManager = stateManager; + this.preferences = preferences; + + BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); + this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); + } + + @Override + public void execute() { + stateManager.getActiveDatabase().ifPresent(databaseContext -> { + final List bibEntries = stateManager.getSelectedEntries(); + + if (bibEntries.size() != 1) { + dialogService.notify(Localization.lang("This operation requires exactly one item to be selected.")); + return; + } + ExternalLinkCreator.getGoogleScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> { + try { + NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); + } catch (IOException ex) { + dialogService.showErrorDialogAndWait(Localization.lang("Unable to open Google Scholar."), ex); + } + }); + }); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index ed9a6a0362b..3aa5638163c 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -10,6 +10,7 @@ public class ExternalLinkCreator { private static final String SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; + private static final String GOOGLE_SCHOLAR_SEARCH_URL = "https://scholar.google.com/scholar"; /** * Get a URL to the search results of ShortScience for the BibEntry's title @@ -31,4 +32,25 @@ public static Optional getShortScienceSearchURL(BibEntry entry) { return uriBuilder.toString(); }); } + + /** + * Get a URL to the search results of Google Scholar for the BibEntry's title + * + * @param entry The entry to search for. Expects the BibEntry's title to be set for successful return. + * @return The URL if it was successfully created + */ + public static Optional getGoogleScholarSearchURL(BibEntry entry) { + return entry.getField(StandardField.TITLE).map(title -> { + URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(GOOGLE_SCHOLAR_SEARCH_URL); + } catch (URISyntaxException e) { + // This should never be able to happen as it would require the field to be misconfigured. + throw new AssertionError("Google Scholar URL is invalid.", e); + } + // Direct the user to the search results for the title. + uriBuilder.addParameter("q", title); + return uriBuilder.toString(); + }); + } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 433be54d054..22e4073a4f2 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2209,6 +2209,9 @@ Text\ editor=Text editor Search\ ShortScience=Search ShortScience Unable\ to\ open\ ShortScience.=Unable to open ShortScience. +Search\ Google\ Scholar=Search Google Scholar +Unable\ to\ open\ Google\ Scholar.=Unable to open Google Scholar. + Shared\ database=Shared database Lookup=Lookup From 9deeb9c714b578878ee596165456ff4d664700d8 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Thu, 29 May 2025 00:05:55 -0700 Subject: [PATCH 02/11] feat: move search short science and google scholar under search sub-menu --- .../org/jabref/gui/actions/StandardActions.java | 1 + .../org/jabref/gui/maintable/RightClickMenu.java | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 9b9c7046e53..9e959d787bf 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -38,6 +38,7 @@ public enum StandardActions implements Action { OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), SEARCH_GOOGLE_SCHOLAR(Localization.lang("Search Google Scholar")), + SEARCH(Localization.lang("Search...")), MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/..."), KeyBinding.MERGE_WITH_FETCHED_ENTRY), BATCH_MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0 (fully automated)", "DOI/ISBN/...")), ATTACH_FILE(Localization.lang("Attach file"), IconTheme.JabRefIcons.ATTACH_FILE), diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index 18185633af2..f211b75845d 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -101,9 +101,7 @@ public static ContextMenu create(BibEntryTableViewModel entry, extractFileReferencesOffline, factory.createMenuItem(StandardActions.OPEN_URL, new OpenUrlAction(dialogService, stateManager, preferences)), - factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences)), - factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences)), - + createSearchSubMenu(factory, dialogService, stateManager, preferences), new SeparatorMenuItem(), new ChangeEntryTypeMenu(libraryTab.getSelectedEntries(), libraryTab.getBibDatabaseContext(), undoManager, entryTypesManager).asSubMenu(), @@ -220,4 +218,16 @@ private static Menu createSendSubMenu(ActionFactory factory, return sendMenu; } + + private static Menu createSearchSubMenu(ActionFactory factory, + DialogService dialogService, + StateManager stateManager, + GuiPreferences preferences) { + Menu searchMenu = factory.createMenu(StandardActions.SEARCH); + searchMenu.getItems().addAll( + factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences)), + factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences)) + ); + return searchMenu; + } } From eab0d25e29475231680bcdf8f5cb0a51f12072f4 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 15:47:25 -0700 Subject: [PATCH 03/11] feat: allow shortscience and googlescholar search to be configurable --- .../org/jabref/gui/maintable/MainTable.java | 27 +++---- .../jabref/gui/maintable/RightClickMenu.java | 13 ++-- .../maintable/SearchGoogleScholarAction.java | 7 +- .../maintable/SearchShortScienceAction.java | 7 +- .../websearch/SearchEngineItem.java | 34 +++++++++ .../preferences/websearch/WebSearchTab.java | 14 ++++ .../websearch/WebSearchTabViewModel.java | 31 ++++++++ .../preferences/websearch/WebSearchTab.fxml | 37 ++++++++-- .../logic/importer/ImporterPreferences.java | 17 ++++- .../preferences/JabRefCliPreferences.java | 3 +- .../logic/util/ExternalLinkCreator.java | 74 +++++++++++++------ 11 files changed, 208 insertions(+), 56 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java index 62a840904d6..4c6d3b67b4d 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -163,7 +163,8 @@ public MainTable(MainTableDataModel model, taskExecutor, Injector.instantiateModelOrService(JournalAbbreviationRepository.class), entryTypesManager, - importHandler)) + importHandler, + preferences.getImporterPreferences())) .withPseudoClass(MATCHING_SEARCH_AND_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_AND_GROUPS)) .withPseudoClass(MATCHING_SEARCH_NOT_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_NOT_GROUPS)) .withPseudoClass(MATCHING_GROUPS_NOT_SEARCH, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_GROUPS_NOT_SEARCH)) @@ -180,10 +181,10 @@ public MainTable(MainTableDataModel model, // force match category column to be the first sort order, (match_category column is always the first column) this.getSortOrder().addFirst(getColumns().getFirst()); this.getSortOrder().addListener((ListChangeListener>) change -> { - if (!this.getSortOrder().getFirst().equals(getColumns().getFirst())) { - this.getSortOrder().addFirst(getColumns().getFirst()); - } - }); + if (!this.getSortOrder().getFirst().equals(getColumns().getFirst())) { + this.getSortOrder().addFirst(getColumns().getFirst()); + } + }); mainTablePreferences.getColumnPreferences().getColumnSortOrder().forEach(columnModel -> this.getColumns().stream() @@ -414,7 +415,7 @@ private void setupKeyBindings(KeyBindingRepository keyBindings) { event.consume(); break; case SCROLL_TO_PREVIOUS_MATCH_CATEGORY: - scrollToPreviousMatchCategory(); + scrollToPreviousMatchCategory(); event.consume(); break; case OPEN_URL_OR_DOI: @@ -576,13 +577,13 @@ public void setCitationMergeMode(boolean citationMerge) { } private void updatePlaceholder(VBox placeholderBox) { - if (database.getDatabase().getEntries().isEmpty()) { - this.setPlaceholder(placeholderBox); - // [impl->req~maintable.focus~1] - requestFocus(); - } else { - this.setPlaceholder(null); - } + if (database.getDatabase().getEntries().isEmpty()) { + this.setPlaceholder(placeholderBox); + // [impl->req~maintable.focus~1] + requestFocus(); + } else { + this.setPlaceholder(null); + } } private BibEntry addExampleEntry() { diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index f211b75845d..683efef998b 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -36,6 +36,7 @@ import org.jabref.gui.specialfields.SpecialFieldMenuItemFactory; import org.jabref.logic.citationstyle.CitationStyleOutputFormat; import org.jabref.logic.citationstyle.CitationStylePreviewLayout; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; @@ -62,7 +63,8 @@ public static ContextMenu create(BibEntryTableViewModel entry, TaskExecutor taskExecutor, JournalAbbreviationRepository abbreviationRepository, BibEntryTypesManager entryTypesManager, - ImportHandler importHandler) { + ImportHandler importHandler, + ImporterPreferences importerPreferences) { ActionFactory factory = new ActionFactory(); ContextMenu contextMenu = new ContextMenu(); @@ -101,7 +103,7 @@ public static ContextMenu create(BibEntryTableViewModel entry, extractFileReferencesOffline, factory.createMenuItem(StandardActions.OPEN_URL, new OpenUrlAction(dialogService, stateManager, preferences)), - createSearchSubMenu(factory, dialogService, stateManager, preferences), + createSearchSubMenu(factory, dialogService, stateManager, preferences, importerPreferences), new SeparatorMenuItem(), new ChangeEntryTypeMenu(libraryTab.getSelectedEntries(), libraryTab.getBibDatabaseContext(), undoManager, entryTypesManager).asSubMenu(), @@ -222,11 +224,12 @@ private static Menu createSendSubMenu(ActionFactory factory, private static Menu createSearchSubMenu(ActionFactory factory, DialogService dialogService, StateManager stateManager, - GuiPreferences preferences) { + GuiPreferences preferences, + ImporterPreferences importerPreferences) { Menu searchMenu = factory.createMenu(StandardActions.SEARCH); searchMenu.getItems().addAll( - factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences)), - factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences)) + factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences, importerPreferences)), + factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences, importerPreferences)) ); return searchMenu; } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java index c135dfb2e31..d3992fce309 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java @@ -10,6 +10,7 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.ExternalLinkCreator; import org.jabref.model.entry.BibEntry; @@ -21,11 +22,13 @@ public class SearchGoogleScholarAction extends SimpleCommand { private final DialogService dialogService; private final StateManager stateManager; private final GuiPreferences preferences; + private final ExternalLinkCreator externalLinkCreator; - public SearchGoogleScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) { + public SearchGoogleScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, ImporterPreferences importerPreferences) { this.dialogService = dialogService; this.stateManager = stateManager; this.preferences = preferences; + this.externalLinkCreator = new ExternalLinkCreator(importerPreferences); BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); @@ -40,7 +43,7 @@ public void execute() { dialogService.notify(Localization.lang("This operation requires exactly one item to be selected.")); return; } - ExternalLinkCreator.getGoogleScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> { + externalLinkCreator.getGoogleScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> { try { NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); } catch (IOException ex) { diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java index 4b6a251c4ff..a8220e9765d 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java @@ -10,6 +10,7 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.ExternalLinkCreator; import org.jabref.model.entry.BibEntry; @@ -22,11 +23,13 @@ public class SearchShortScienceAction extends SimpleCommand { private final DialogService dialogService; private final StateManager stateManager; private final GuiPreferences preferences; + private final ExternalLinkCreator externalLinkCreator; - public SearchShortScienceAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) { + public SearchShortScienceAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, ImporterPreferences importerPreferences) { this.dialogService = dialogService; this.stateManager = stateManager; this.preferences = preferences; + this.externalLinkCreator = new ExternalLinkCreator(importerPreferences); BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); @@ -41,7 +44,7 @@ public void execute() { dialogService.notify(Localization.lang("This operation requires exactly one item to be selected.")); return; } - ExternalLinkCreator.getShortScienceSearchURL(bibEntries.getFirst()).ifPresent(url -> { + externalLinkCreator.getShortScienceSearchURL(bibEntries.getFirst()).ifPresent(url -> { try { NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); } catch (IOException ex) { diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java new file mode 100644 index 00000000000..c10d3f79f0b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java @@ -0,0 +1,34 @@ +package org.jabref.gui.preferences.websearch; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class SearchEngineItem { + private final StringProperty name; + private final StringProperty urlTemplate; + + public SearchEngineItem(String name, String urlTemplate) { + this.name = new SimpleStringProperty(name); + this.urlTemplate = new SimpleStringProperty(urlTemplate); + } + + public StringProperty nameProperty() { + return name; + } + + public StringProperty urlTemplateProperty() { + return urlTemplate; + } + + public String getName() { + return name.get(); + } + + public String getUrlTemplate() { + return urlTemplate.get(); + } + + public void setUrlTemplate(String urlTemplate) { + this.urlTemplate.set(urlTemplate); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java index 6a2f75f83b4..93797a07cb3 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java @@ -45,6 +45,10 @@ public class WebSearchTab extends AbstractPreferenceTabView searchEngineTable; + @FXML private TableColumn searchEngineName; + @FXML private TableColumn searchEngineUrlTemplate; + @FXML private TableView apiKeySelectorTable; @FXML private TableColumn apiKeyName; @FXML private TableColumn customApiKey; @@ -75,6 +79,16 @@ public String getTabName() { public void initialize() { this.viewModel = new WebSearchTabViewModel(preferences, dialogService, refAiEnabled); + searchEngineName.setCellValueFactory(param -> param.getValue().nameProperty()); + searchEngineName.setCellFactory(TextFieldTableCell.forTableColumn()); + searchEngineName.setEditable(false); + + searchEngineUrlTemplate.setCellValueFactory(param -> param.getValue().urlTemplateProperty()); + searchEngineUrlTemplate.setCellFactory(TextFieldTableCell.forTableColumn()); + searchEngineUrlTemplate.setEditable(true); + + searchEngineTable.setItems(viewModel.getSearchEngines()); + enableWebSearch.selectedProperty().bindBidirectional(viewModel.enableWebSearchProperty()); warnAboutDuplicatesOnImport.selectedProperty().bindBidirectional(viewModel.warnAboutDuplicatesOnImportProperty()); downloadLinkedOnlineFiles.selectedProperty().bindBidirectional(viewModel.shouldDownloadLinkedOnlineFiles()); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java index f5785fd2933..4cff4582543 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.net.HttpURLConnection; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -68,6 +69,8 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel { private final BooleanProperty apikeyPersistProperty = new SimpleBooleanProperty(); private final BooleanProperty apikeyPersistAvailableProperty = new SimpleBooleanProperty(); + private final ObservableList searchEngines = FXCollections.observableArrayList(); + private final DialogService dialogService; private final CliPreferences preferences; private final DOIPreferences doiPreferences; @@ -92,6 +95,7 @@ public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogSer this.refAiEnabled = refAiEnabled; setupPlainCitationParsers(preferences); + setupSearchEngines(); } private void setupPlainCitationParsers(CliPreferences preferences) { @@ -132,6 +136,14 @@ private void setupPlainCitationParsers(CliPreferences preferences) { }); } + private void setupSearchEngines() { + // Add default search engines + searchEngines.addAll( + new SearchEngineItem("Google Scholar", "https://scholar.google.com/scholar?q={title}"), + new SearchEngineItem("Short Science", "https://www.shortscience.org/internalsearch?q={title}") + ); + } + @Override public void setValues() { enableWebSearchProperty.setValue(importerPreferences.areImporterEnabled()); @@ -164,6 +176,13 @@ public void setValues() { return new StudyCatalogItem(name, enabled); }) .toList()); + + // Load custom URL templates from preferences if they exist + Map savedTemplates = preferences.getImporterPreferences().getSearchEngineUrlTemplates(); + if (!savedTemplates.isEmpty()) { + searchEngines.clear(); + savedTemplates.forEach((name, url) -> searchEngines.add(new SearchEngineItem(name, url))); + } } @Override @@ -196,6 +215,14 @@ public void storeSettings() { if (apikeyPersistAvailableProperty.get()) { preferences.getImporterPreferences().getApiKeys().addAll(apiKeys); } + + // Save custom URL templates to preferences + Map templates = searchEngines.stream() + .collect(Collectors.toMap( + SearchEngineItem::getName, + SearchEngineItem::getUrlTemplate + )); + preferences.getImporterPreferences().setSearchEngineUrlTemplates(templates); } public BooleanProperty enableWebSearchProperty() { @@ -270,6 +297,10 @@ public IntegerProperty citationsRelationsStoreTTLProperty() { return citationsRelationStoreTTL; } + public ObservableList getSearchEngines() { + return searchEngines; + } + public void checkCustomApiKey() { final String apiKeyName = selectedApiKeyProperty.get().getName(); diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml index 987561b711a..db7a4a1f095 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml @@ -11,6 +11,7 @@ + @@ -46,9 +47,9 @@ editable="true"> + fx:id="catalogEnabledColumn" + text="%Enabled" + /> @@ -66,6 +67,28 @@ + catalogs; private final ObjectProperty defaultPlainCitationParser; private final IntegerProperty citationsRelationsStoreTTL; + private final Map searchEngineUrlTemplates; public ImporterPreferences(boolean importerEnabled, boolean generateNewKeyOnImport, @@ -43,7 +45,8 @@ public ImporterPreferences(boolean importerEnabled, boolean persistCustomKeys, List catalogs, PlainCitationParserChoice defaultPlainCitationParser, - int citationsRelationsStoreTTL + int citationsRelationsStoreTTL, + Map searchEngineUrlTemplates ) { this.importerEnabled = new SimpleBooleanProperty(importerEnabled); this.generateNewKeyOnImport = new SimpleBooleanProperty(generateNewKeyOnImport); @@ -56,6 +59,7 @@ public ImporterPreferences(boolean importerEnabled, this.catalogs = FXCollections.observableArrayList(catalogs); this.defaultPlainCitationParser = new SimpleObjectProperty<>(defaultPlainCitationParser); this.citationsRelationsStoreTTL = new SimpleIntegerProperty(citationsRelationsStoreTTL); + this.searchEngineUrlTemplates = new HashMap<>(searchEngineUrlTemplates); } public boolean areImporterEnabled() { @@ -150,7 +154,7 @@ public void setCatalogs(List catalogs) { } public ObservableList getCatalogs() { - return catalogs; + return catalogs; } public PlainCitationParserChoice getDefaultPlainCitationParser() { @@ -176,4 +180,13 @@ public IntegerProperty citationsRelationsStoreTTLProperty() { public void setCitationsRelationsStoreTTL(int citationsRelationsStoreTTL) { this.citationsRelationsStoreTTL.set(citationsRelationsStoreTTL); } + + public Map getSearchEngineUrlTemplates() { + return searchEngineUrlTemplates; + } + + public void setSearchEngineUrlTemplates(Map templates) { + searchEngineUrlTemplates.clear(); + searchEngineUrlTemplates.putAll(templates); + } } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index bd35b70f208..634fe499e5b 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -2046,7 +2046,8 @@ public ImporterPreferences getImporterPreferences() { getBoolean(FETCHER_CUSTOM_KEY_PERSIST), getStringList(SEARCH_CATALOGS), PlainCitationParserChoice.valueOf(get(DEFAULT_PLAIN_CITATION_PARSER)), - getInt(CITATIONS_RELATIONS_STORE_TTL) + getInt(CITATIONS_RELATIONS_STORE_TTL), + Map.of() ); EasyBind.listen(importerPreferences.importerEnabledProperty(), (_, _, newValue) -> putBoolean(IMPORTERS_ENABLED, newValue)); diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index 3aa5638163c..f11e3e3b5c7 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -3,14 +3,20 @@ import java.net.URISyntaxException; import java.util.Optional; +import org.apache.hc.core5.net.URIBuilder; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; -import org.apache.hc.core5.net.URIBuilder; - public class ExternalLinkCreator { - private static final String SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; - private static final String GOOGLE_SCHOLAR_SEARCH_URL = "https://scholar.google.com/scholar"; + private static final String DEFAULT_SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; + private static final String DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL = "https://scholar.google.com/scholar"; + + private final ImporterPreferences importerPreferences; + + public ExternalLinkCreator(ImporterPreferences importerPreferences) { + this.importerPreferences = importerPreferences; + } /** * Get a URL to the search results of ShortScience for the BibEntry's title @@ -18,18 +24,28 @@ public class ExternalLinkCreator { * @param entry The entry to search for. Expects the BibEntry's title to be set for successful return. * @return The URL if it was successfully created */ - public static Optional getShortScienceSearchURL(BibEntry entry) { + public Optional getShortScienceSearchURL(BibEntry entry) { return entry.getField(StandardField.TITLE).map(title -> { - URIBuilder uriBuilder; - try { - uriBuilder = new URIBuilder(SHORTSCIENCE_SEARCH_URL); - } catch (URISyntaxException e) { - // This should never be able to happen as it would require the field to be misconfigured. - throw new AssertionError("ShortScience URL is invalid.", e); + // Use custom URL template if available, otherwise use default + String baseUrl = importerPreferences.getSearchEngineUrlTemplates() + .getOrDefault("Short Science", DEFAULT_SHORTSCIENCE_SEARCH_URL); + + Optional author = entry.getField(StandardField.AUTHOR); + + // If URL doesn't contain {title}, it's invalid, use default + if (!baseUrl.contains("{title}")) { + try { + URIBuilder uriBuilder = new URIBuilder(DEFAULT_SHORTSCIENCE_SEARCH_URL); + uriBuilder.addParameter("q", title); + author.ifPresent(a -> uriBuilder.addParameter("author", a)); + return uriBuilder.toString(); + } catch (URISyntaxException ex) { + throw new AssertionError("ShortScience URL is invalid.", ex); + } } - // Direct the user to the search results for the title. - uriBuilder.addParameter("q", title.trim()); - return uriBuilder.toString(); + + String urlWithTitle = baseUrl.replace("{title}", title); + return author.map(a -> urlWithTitle.replace("{author}", a)).orElse(urlWithTitle); }); } @@ -39,18 +55,28 @@ public static Optional getShortScienceSearchURL(BibEntry entry) { * @param entry The entry to search for. Expects the BibEntry's title to be set for successful return. * @return The URL if it was successfully created */ - public static Optional getGoogleScholarSearchURL(BibEntry entry) { + public Optional getGoogleScholarSearchURL(BibEntry entry) { return entry.getField(StandardField.TITLE).map(title -> { - URIBuilder uriBuilder; - try { - uriBuilder = new URIBuilder(GOOGLE_SCHOLAR_SEARCH_URL); - } catch (URISyntaxException e) { - // This should never be able to happen as it would require the field to be misconfigured. - throw new AssertionError("Google Scholar URL is invalid.", e); + // Use custom URL template if available, otherwise use default + String baseUrl = importerPreferences.getSearchEngineUrlTemplates() + .getOrDefault("Google Scholar", DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL); + + Optional author = entry.getField(StandardField.AUTHOR); + + // If URL doesn't contain {title}, it's invalid, use default + if (!baseUrl.contains("{title}")) { + try { + URIBuilder uriBuilder = new URIBuilder(DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL); + uriBuilder.addParameter("q", title); + author.ifPresent(a -> uriBuilder.addParameter("author", a)); + return uriBuilder.toString(); + } catch (URISyntaxException ex) { + throw new AssertionError("Default Google Scholar URL is invalid.", ex); + } } - // Direct the user to the search results for the title. - uriBuilder.addParameter("q", title); - return uriBuilder.toString(); + + String urlWithTitle = baseUrl.replace("{title}", title); + return author.map(a -> urlWithTitle.replace("{author}", a)).orElse(urlWithTitle); }); } } From 667308a28928b9b88b1194f005781a2fc1a6f95f Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 16:11:11 -0700 Subject: [PATCH 04/11] feat: update change log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 234387fd283..fc0eee5a918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added support for import of a Refer/BibIX file format. [#13069](https://github.com/JabRef/jabref/issues/13069) - We added a new `jabkit` command `pseudonymize` to pseudonymize the library. [#13109](https://github.com/JabRef/jabref/issues/13109) - We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129) +- We added support for "Search Google Scholar" to quickly search for a selected entry's title in Google Scholar directly from the main table's context menu [#12268](https://github.com/JabRef/jabref/issues/12268) +- We introduced a new "Search Engine URL Template" setting in Preferences to allow users to customize their search engine URL templates [#12268](https://github.com/JabRef/jabref/issues/12268) ### Changed From 9f06f1b3a0025da995aca6ed042b4e4aa8598f60 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 16:19:34 -0700 Subject: [PATCH 05/11] feat: fix tests --- .../logic/util/ExternalLinkCreatorTest.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java index b26c177be40..f78c8f73890 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -1,9 +1,11 @@ package org.jabref.logic.util; import java.net.MalformedURLException; +import java.util.Collections; import java.util.Optional; import java.util.stream.Stream; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; @@ -13,12 +15,30 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -import static org.jabref.logic.util.ExternalLinkCreator.getShortScienceSearchURL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class ExternalLinkCreatorTest { + // Create stub for ImporterPreferences to test + private static class StubImporterPreferences extends ImporterPreferences { + public StubImporterPreferences() { + super( + true, // importerEnabled + true, // generateNewKeyOnImport + null, // importWorkingDirectory + true, // warnAboutDuplicatesOnImport + Collections.emptySet(), // customImporters + Collections.emptySet(), // apiKeys + Collections.emptyMap(), // defaultApiKeys + true, // persistCustomKeys + Collections.emptyList(), // catalogs + null, // defaultPlainCitationParser + Collections.emptyMap() // searchEngineUrlTemplates + ); + } + } + /** * Validates URL conformance to RFC2396. Does not perform complex checks such as opening connections. */ @@ -63,7 +83,22 @@ void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) @Test void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { + ImporterPreferences stubPreferences = new StubImporterPreferences(); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + BibEntry entry = new BibEntry(); - assertEquals(Optional.empty(), getShortScienceSearchURL(entry)); + assertEquals(Optional.empty(), linkCreator.getShortScienceSearchURL(entry)); + } + + @Test + void getShortScienceSearchURLLinksToSearchResults() { + ImporterPreferences stubPreferences = new StubImporterPreferences(); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + + // Take an arbitrary article name + BibEntry entry = new BibEntry().withField(StandardField.TITLE, "JabRef bibliography management"); + Optional url = linkCreator.getShortScienceSearchURL(entry); + // Expected behaviour is to link to the search results page, /internalsearch + assertEquals(Optional.of("https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management"), url); } } From 2a33b4e597abe12e3267f21644e403f5f96cad7a Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 16:33:18 -0700 Subject: [PATCH 06/11] feat: fix import errors --- .../jabref/gui/maintable/SearchGoogleScholarAction.java | 7 ++++--- .../java/org/jabref/logic/util/ExternalLinkCreator.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java index d3992fce309..64d0fbaa86a 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java @@ -3,10 +3,10 @@ import java.io.IOException; import java.util.List; +import javafx.beans.binding.BooleanExpression; + import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; -import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry; -import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.preferences.GuiPreferences; @@ -16,7 +16,8 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; -import javafx.beans.binding.BooleanExpression; +import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry; +import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; public class SearchGoogleScholarAction extends SimpleCommand { private final DialogService dialogService; diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index f11e3e3b5c7..9b71f72e547 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -4,9 +4,9 @@ import java.util.Optional; import org.apache.hc.core5.net.URIBuilder; -import org.jabref.logic.importer.ImporterPreferences; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.logic.importer.ImporterPreferences; public class ExternalLinkCreator { private static final String DEFAULT_SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; From d5b7033594b3d907edb3f68cca3e721a4353e4d1 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 16:40:19 -0700 Subject: [PATCH 07/11] test to use List.of() --- .../java/org/jabref/logic/util/ExternalLinkCreatorTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java index f78c8f73890..31ba6777804 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -2,6 +2,7 @@ import java.net.MalformedURLException; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -32,7 +33,7 @@ public StubImporterPreferences() { Collections.emptySet(), // apiKeys Collections.emptyMap(), // defaultApiKeys true, // persistCustomKeys - Collections.emptyList(), // catalogs + List.of(), // catalogs null, // defaultPlainCitationParser Collections.emptyMap() // searchEngineUrlTemplates ); From 2d65f10f82350f7c0cabf71f9041831f422715f5 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Fri, 30 May 2025 16:46:31 -0700 Subject: [PATCH 08/11] fix import err --- .../main/java/org/jabref/logic/util/ExternalLinkCreator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index 9b71f72e547..1cb32e9bfda 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -4,9 +4,10 @@ import java.util.Optional; import org.apache.hc.core5.net.URIBuilder; + +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; -import org.jabref.logic.importer.ImporterPreferences; public class ExternalLinkCreator { private static final String DEFAULT_SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; From eade1cf5b55ac04b080dd746862a57e6db6b1b05 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Sat, 7 Jun 2025 18:11:41 -0700 Subject: [PATCH 09/11] fix merge conflicts and external link creator tests --- .../logic/util/ExternalLinkCreator.java | 4 +-- .../logic/util/ExternalLinkCreatorTest.java | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index 1cb32e9bfda..fec9a5e1689 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -37,7 +37,7 @@ public Optional getShortScienceSearchURL(BibEntry entry) { if (!baseUrl.contains("{title}")) { try { URIBuilder uriBuilder = new URIBuilder(DEFAULT_SHORTSCIENCE_SEARCH_URL); - uriBuilder.addParameter("q", title); + uriBuilder.addParameter("q", title.trim()); author.ifPresent(a -> uriBuilder.addParameter("author", a)); return uriBuilder.toString(); } catch (URISyntaxException ex) { @@ -68,7 +68,7 @@ public Optional getGoogleScholarSearchURL(BibEntry entry) { if (!baseUrl.contains("{title}")) { try { URIBuilder uriBuilder = new URIBuilder(DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL); - uriBuilder.addParameter("q", title); + uriBuilder.addParameter("q", title.trim()); author.ifPresent(a -> uriBuilder.addParameter("author", a)); return uriBuilder.toString(); } catch (URISyntaxException ex) { diff --git a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java index 31ba6777804..6ec3f5240ff 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -35,6 +35,7 @@ public StubImporterPreferences() { true, // persistCustomKeys List.of(), // catalogs null, // defaultPlainCitationParser + 5, // citationsRelationsStoreTTL Collections.emptyMap() // searchEngineUrlTemplates ); } @@ -62,9 +63,12 @@ static Stream specialCharactersProvider() { @ParameterizedTest @MethodSource("specialCharactersProvider") void getShortScienceSearchURLEncodesSpecialCharacters(String title) { + ImporterPreferences stubPreferences = new StubImporterPreferences(); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, title); - Optional url = getShortScienceSearchURL(entry); + Optional url = linkCreator.getShortScienceSearchURL(entry); assertTrue(url.isPresent()); assertTrue(urlIsValid(url.get())); } @@ -77,8 +81,11 @@ void getShortScienceSearchURLEncodesSpecialCharacters(String title) { "'JabRef bibliography management', 'https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management'" }) void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) { + ImporterPreferences stubPreferences = new StubImporterPreferences(); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); - Optional url = getShortScienceSearchURL(entry); + Optional url = linkCreator.getShortScienceSearchURL(entry); assertEquals(Optional.of(expectedUrl), url); } @@ -91,15 +98,17 @@ void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { assertEquals(Optional.empty(), linkCreator.getShortScienceSearchURL(entry)); } - @Test - void getShortScienceSearchURLLinksToSearchResults() { + @ParameterizedTest + @CsvSource({ + "JabRef bibliography management, https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management", + "Machine learning, https://www.shortscience.org/internalsearch?q=Machine%20learning", + }) + void getShortScienceSearchURLLinksToSearchResults(String title, String expectedUrl) { ImporterPreferences stubPreferences = new StubImporterPreferences(); ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); - // Take an arbitrary article name - BibEntry entry = new BibEntry().withField(StandardField.TITLE, "JabRef bibliography management"); + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); Optional url = linkCreator.getShortScienceSearchURL(entry); - // Expected behaviour is to link to the search results page, /internalsearch - assertEquals(Optional.of("https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management"), url); + assertEquals(Optional.of(expectedUrl), url); } } From 8463aeb7bfe70974feeb2d9a577bbcddd4ce7f33 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Sat, 7 Jun 2025 19:06:03 -0700 Subject: [PATCH 10/11] fix test to use mock for importerpreferences --- .../logic/util/ExternalLinkCreatorTest.java | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java index 6ec3f5240ff..f16a8acf7dc 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -16,31 +16,11 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import static org.mockito.Mockito.mock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class ExternalLinkCreatorTest { - - // Create stub for ImporterPreferences to test - private static class StubImporterPreferences extends ImporterPreferences { - public StubImporterPreferences() { - super( - true, // importerEnabled - true, // generateNewKeyOnImport - null, // importWorkingDirectory - true, // warnAboutDuplicatesOnImport - Collections.emptySet(), // customImporters - Collections.emptySet(), // apiKeys - Collections.emptyMap(), // defaultApiKeys - true, // persistCustomKeys - List.of(), // catalogs - null, // defaultPlainCitationParser - 5, // citationsRelationsStoreTTL - Collections.emptyMap() // searchEngineUrlTemplates - ); - } - } - /** * Validates URL conformance to RFC2396. Does not perform complex checks such as opening connections. */ @@ -63,11 +43,10 @@ static Stream specialCharactersProvider() { @ParameterizedTest @MethodSource("specialCharactersProvider") void getShortScienceSearchURLEncodesSpecialCharacters(String title) { - ImporterPreferences stubPreferences = new StubImporterPreferences(); - ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); - BibEntry entry = new BibEntry(); - entry.setField(StandardField.TITLE, title); + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); Optional url = linkCreator.getShortScienceSearchURL(entry); assertTrue(url.isPresent()); assertTrue(urlIsValid(url.get())); @@ -81,8 +60,8 @@ void getShortScienceSearchURLEncodesSpecialCharacters(String title) { "'JabRef bibliography management', 'https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management'" }) void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) { - ImporterPreferences stubPreferences = new StubImporterPreferences(); - ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); Optional url = linkCreator.getShortScienceSearchURL(entry); @@ -91,8 +70,8 @@ void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) @Test void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { - ImporterPreferences stubPreferences = new StubImporterPreferences(); - ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); BibEntry entry = new BibEntry(); assertEquals(Optional.empty(), linkCreator.getShortScienceSearchURL(entry)); @@ -104,8 +83,8 @@ void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { "Machine learning, https://www.shortscience.org/internalsearch?q=Machine%20learning", }) void getShortScienceSearchURLLinksToSearchResults(String title, String expectedUrl) { - ImporterPreferences stubPreferences = new StubImporterPreferences(); - ExternalLinkCreator linkCreator = new ExternalLinkCreator(stubPreferences); + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); Optional url = linkCreator.getShortScienceSearchURL(entry); From bd0e67b18ecd0392c4eeed15ad62b2e8a6632e85 Mon Sep 17 00:00:00 2001 From: Brandon Lau <1brandonlau@gmail.com> Date: Sat, 7 Jun 2025 19:31:10 -0700 Subject: [PATCH 11/11] support semantic scholar search engine --- CHANGELOG.md | 1 + .../jabref/gui/actions/StandardActions.java | 1 + .../jabref/gui/maintable/RightClickMenu.java | 3 +- .../SearchSemanticScholarAction.java | 56 +++++++++++++++++++ .../websearch/WebSearchTabViewModel.java | 3 +- .../logic/util/ExternalLinkCreator.java | 32 +++++++++++ .../main/resources/l10n/JabRef_en.properties | 3 + 7 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/maintable/SearchSemanticScholarAction.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0eee5a918..04e31060bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129) - We added support for "Search Google Scholar" to quickly search for a selected entry's title in Google Scholar directly from the main table's context menu [#12268](https://github.com/JabRef/jabref/issues/12268) - We introduced a new "Search Engine URL Template" setting in Preferences to allow users to customize their search engine URL templates [#12268](https://github.com/JabRef/jabref/issues/12268) +- We added support for "Search Semantic Scholar" to quickly search for a selected entry's title in Semantic Scholar directly from the main table's context menu ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 9e959d787bf..be4e2965094 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -38,6 +38,7 @@ public enum StandardActions implements Action { OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), SEARCH_GOOGLE_SCHOLAR(Localization.lang("Search Google Scholar")), + SEARCH_SEMANTIC_SCHOLAR(Localization.lang("Search Semantic Scholar")), SEARCH(Localization.lang("Search...")), MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/..."), KeyBinding.MERGE_WITH_FETCHED_ENTRY), BATCH_MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0 (fully automated)", "DOI/ISBN/...")), diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index 683efef998b..142e734823f 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -229,7 +229,8 @@ private static Menu createSearchSubMenu(ActionFactory factory, Menu searchMenu = factory.createMenu(StandardActions.SEARCH); searchMenu.getItems().addAll( factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences, importerPreferences)), - factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences, importerPreferences)) + factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences, importerPreferences)), + factory.createMenuItem(StandardActions.SEARCH_SEMANTIC_SCHOLAR, new SearchSemanticScholarAction(dialogService, stateManager, preferences, importerPreferences)) ); return searchMenu; } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchSemanticScholarAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchSemanticScholarAction.java new file mode 100644 index 00000000000..73a907c5e6e --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchSemanticScholarAction.java @@ -0,0 +1,56 @@ +package org.jabref.gui.maintable; + +import java.io.IOException; +import java.util.List; + +import javafx.beans.binding.BooleanExpression; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.importer.ImporterPreferences; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.ExternalLinkCreator; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry; +import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; + +public class SearchSemanticScholarAction extends SimpleCommand { + private final DialogService dialogService; + private final StateManager stateManager; + private final GuiPreferences preferences; + private final ExternalLinkCreator externalLinkCreator; + + public SearchSemanticScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, ImporterPreferences importerPreferences) { + this.dialogService = dialogService; + this.stateManager = stateManager; + this.preferences = preferences; + this.externalLinkCreator = new ExternalLinkCreator(importerPreferences); + + BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); + this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); + } + + @Override + public void execute() { + stateManager.getActiveDatabase().ifPresent(databaseContext -> { + final List bibEntries = stateManager.getSelectedEntries(); + + if (bibEntries.size() != 1) { + dialogService.notify(Localization.lang("This operation requires exactly one item to be selected.")); + return; + } + externalLinkCreator.getSemanticScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> { + try { + NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); + } catch (IOException ex) { + dialogService.showErrorDialogAndWait(Localization.lang("Unable to open Semantic Scholar."), ex); + } + }); + }); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java index 4cff4582543..7aa373a2355 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java @@ -140,7 +140,8 @@ private void setupSearchEngines() { // Add default search engines searchEngines.addAll( new SearchEngineItem("Google Scholar", "https://scholar.google.com/scholar?q={title}"), - new SearchEngineItem("Short Science", "https://www.shortscience.org/internalsearch?q={title}") + new SearchEngineItem("Short Science", "https://www.shortscience.org/internalsearch?q={title}"), + new SearchEngineItem("Semantic Scholar", "https://www.semanticscholar.org/search?q={title}") ); } diff --git a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index fec9a5e1689..99019a7f82a 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -12,6 +12,7 @@ public class ExternalLinkCreator { private static final String DEFAULT_SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch"; private static final String DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL = "https://scholar.google.com/scholar"; + private static final String DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL = "https://www.semanticscholar.org/search"; private final ImporterPreferences importerPreferences; @@ -80,4 +81,35 @@ public Optional getGoogleScholarSearchURL(BibEntry entry) { return author.map(a -> urlWithTitle.replace("{author}", a)).orElse(urlWithTitle); }); } + + /** + * Get a URL to the search results of Semantic Scholar for the BibEntry's title + * + * @param entry The entry to search for. Expects the BibEntry's title to be set for successful return. + * @return The URL if it was successfully created + */ + public Optional getSemanticScholarSearchURL(BibEntry entry) { + return entry.getField(StandardField.TITLE).map(title -> { + // Use custom URL template if available, otherwise use default + String baseUrl = importerPreferences.getSearchEngineUrlTemplates() + .getOrDefault("Semantic Scholar", DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL); + + Optional author = entry.getField(StandardField.AUTHOR); + + // If URL doesn't contain {title}, it's invalid, use default + if (!baseUrl.contains("{title}")) { + try { + URIBuilder uriBuilder = new URIBuilder(DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL); + uriBuilder.addParameter("q", title); + author.ifPresent(a -> uriBuilder.addParameter("author", a)); + return uriBuilder.toString(); + } catch (URISyntaxException ex) { + throw new AssertionError("Default Semantic Scholar URL is invalid.", ex); + } + } + + String urlWithTitle = baseUrl.replace("{title}", title); + return author.map(a -> urlWithTitle.replace("{author}", a)).orElse(urlWithTitle); + }); + } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 22e4073a4f2..c6c23d218a5 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2212,6 +2212,9 @@ Unable\ to\ open\ ShortScience.=Unable to open ShortScience. Search\ Google\ Scholar=Search Google Scholar Unable\ to\ open\ Google\ Scholar.=Unable to open Google Scholar. +Search\ Semantic\ Scholar=Search Semantic Scholar +Unable\ to\ open\ Semantic\ Scholar.=Unable to open Semantic Scholar. + Shared\ database=Shared database Lookup=Lookup