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 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..9e959d787bf 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,8 @@ 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")), + 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/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 5a9c93e6cac..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,8 +103,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)), - + createSearchSubMenu(factory, dialogService, stateManager, preferences, importerPreferences), new SeparatorMenuItem(), new ChangeEntryTypeMenu(libraryTab.getSelectedEntries(), libraryTab.getBibDatabaseContext(), undoManager, entryTypesManager).asSubMenu(), @@ -219,4 +220,17 @@ private static Menu createSendSubMenu(ActionFactory factory, return sendMenu; } + + private static Menu createSearchSubMenu(ActionFactory factory, + DialogService dialogService, + StateManager stateManager, + GuiPreferences preferences, + ImporterPreferences importerPreferences) { + 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)) + ); + 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 new file mode 100644 index 00000000000..64d0fbaa86a --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.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 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, 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.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/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 ed9a6a0362b..fec9a5e1689 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -3,13 +3,21 @@ 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 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 @@ -17,18 +25,59 @@ 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.trim()); + 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); + }); + } + + /** + * 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 Optional getGoogleScholarSearchURL(BibEntry entry) { + return entry.getField(StandardField.TITLE).map(title -> { + // 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.trim()); + author.ifPresent(a -> uriBuilder.addParameter("author", a)); + return uriBuilder.toString(); + } catch (URISyntaxException ex) { + throw new AssertionError("Default Google 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 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 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..0f29fbef6d6 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -4,6 +4,7 @@ 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 +14,11 @@ 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.mockito.Mockito.mock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class ExternalLinkCreatorTest { - /** * Validates URL conformance to RFC2396. Does not perform complex checks such as opening connections. */ @@ -41,9 +41,11 @@ static Stream specialCharactersProvider() { @ParameterizedTest @MethodSource("specialCharactersProvider") void getShortScienceSearchURLEncodesSpecialCharacters(String title) { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.TITLE, title); - Optional url = getShortScienceSearchURL(entry); + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); + + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); + Optional url = linkCreator.getShortScienceSearchURL(entry); assertTrue(url.isPresent()); assertTrue(urlIsValid(url.get())); } @@ -56,14 +58,34 @@ void getShortScienceSearchURLEncodesSpecialCharacters(String title) { "'JabRef bibliography management', 'https://www.shortscience.org/internalsearch?q=JabRef%20bibliography%20management'" }) void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) { + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); - Optional url = getShortScienceSearchURL(entry); + Optional url = linkCreator.getShortScienceSearchURL(entry); assertEquals(Optional.of(expectedUrl), url); } @Test void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { + ImporterPreferences mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); + BibEntry entry = new BibEntry(); - assertEquals(Optional.empty(), getShortScienceSearchURL(entry)); + assertEquals(Optional.empty(), linkCreator.getShortScienceSearchURL(entry)); + } + + @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 mockPreferences = mock(ImporterPreferences.class); + ExternalLinkCreator linkCreator = new ExternalLinkCreator(mockPreferences); + + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); + Optional url = linkCreator.getShortScienceSearchURL(entry); + assertEquals(Optional.of(expectedUrl), url); } }