diff --git a/CHANGELOG.md b/CHANGELOG.md index ff612b3f49d..2334c3e31cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added automatic lookup of DOI at citation relations [#13234](https://github.com/JabRef/jabref/issues/13234) - We added focus on the field Link in the "Add file link" dialog. [#13486](https://github.com/JabRef/jabref/issues/13486) - We introduced a settings parameter to manage citations' relations local storage time-to-live with a default value set to 30 days. [#11189](https://github.com/JabRef/jabref/issues/11189) +- We added context-menu actions to search Google Scholar or Semantic Scholar using the selected entry's title. [#12268](https://github.com/JabRef/jabref/issues/12268) - We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842) - When adding an entry to a library, a warning is displayed if said entry already exists in an active library. [#13261](https://github.com/JabRef/jabref/issues/13261) - We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375) 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 0c37633c8c4..5ec7b9fec1a 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -45,6 +45,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_SEMANTIC_SCHOLAR(Localization.lang("Search Semantic 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 16d67dcaf49..df90a3f08e8 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -100,6 +100,8 @@ 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)), + factory.createMenuItem(StandardActions.SEARCH_SEMANTIC_SCHOLAR, new SearchSemanticScholarAction(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..9a129bd8c1f --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java @@ -0,0 +1,54 @@ +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.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; + + 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/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..302c532792c --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchSemanticScholarAction.java @@ -0,0 +1,54 @@ +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.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; + + public SearchSemanticScholarAction(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.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/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java index 32764d1a2c4..71e320c4976 100644 --- a/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java +++ b/jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java @@ -11,6 +11,8 @@ 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 SEMANTIC_SCHOLAR_SEARCH_URL = "https://www.semanticscholar.org/search"; /** * Get a URL to the search results of ShortScience for the BibEntry's title @@ -37,4 +39,47 @@ 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); + } + + String filteredTitle = LatexToUnicodeAdapter.format(title); + uriBuilder.addParameter("q", filteredTitle.trim()); + return uriBuilder.toString(); + }); + } + + /** + * 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 static Optional getSemanticScholarSearchURL(BibEntry entry) { + return entry.getField(StandardField.TITLE).map(title -> { + URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(SEMANTIC_SCHOLAR_SEARCH_URL); + } catch (URISyntaxException e) { + throw new AssertionError("Semantic Scholar URL is invalid.", e); + } + + String filteredTitle = LatexToUnicodeAdapter.format(title); + uriBuilder.addParameter("q", filteredTitle.trim()); + return uriBuilder.toString(); + }); + } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 9b1b266fd5d..8b8a577abff 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2292,6 +2292,10 @@ 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. +Search\ Semantic\ Scholar=Search Semantic Scholar +Unable\ to\ open\ Semantic\ Scholar.=Unable to open Semantic 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 2f1165f84cf..72fd83a0403 100644 --- a/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/ExternalLinkCreatorTest.java @@ -13,6 +13,8 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import static org.jabref.logic.util.ExternalLinkCreator.getGoogleScholarSearchURL; +import static org.jabref.logic.util.ExternalLinkCreator.getSemanticScholarSearchURL; import static org.jabref.logic.util.ExternalLinkCreator.getShortScienceSearchURL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -48,6 +50,26 @@ void getShortScienceSearchURLEncodesSpecialCharacters(String title) { assertTrue(urlIsValid(url.get())); } + @ParameterizedTest + @MethodSource("specialCharactersProvider") + void getGoogleScholarSearchURLEncodesSpecialCharacters(String title) { + BibEntry entry = new BibEntry(); + entry.setField(StandardField.TITLE, title); + Optional url = getGoogleScholarSearchURL(entry); + assertTrue(url.isPresent()); + assertTrue(urlIsValid(url.get())); + } + + @ParameterizedTest + @MethodSource("specialCharactersProvider") + void getSemanticScholarSearchURLEncodesSpecialCharacters(String title) { + BibEntry entry = new BibEntry(); + entry.setField(StandardField.TITLE, title); + Optional url = getSemanticScholarSearchURL(entry); + assertTrue(url.isPresent()); + assertTrue(urlIsValid(url.get())); + } + @ParameterizedTest @CsvSource({ "'歷史書 📖 📚', 'https://www.shortscience.org/internalsearch?q=%E6%AD%B7%E5%8F%B2%E6%9B%B8%20%F0%9F%93%96%20%F0%9F%93%9A'", @@ -61,12 +83,50 @@ void getShortScienceSearchURLEncodesCharacters(String title, String expectedUrl) assertEquals(Optional.of(expectedUrl), url); } + @ParameterizedTest + @CsvSource({ + "'歷史書 📖 📚', 'https://scholar.google.com/scholar?q=%E6%AD%B7%E5%8F%B2%E6%9B%B8%20%F0%9F%93%96%20%F0%9F%93%9A'", + "' History Textbook ', 'https://scholar.google.com/scholar?q=History%20Textbook'", + "'History%20Textbook', 'https://scholar.google.com/scholar?q=History%2520Textbook'", + "'JabRef bibliography management', 'https://scholar.google.com/scholar?q=JabRef%20bibliography%20management'" + }) + void getGoogleScholarSearchURLEncodesCharacters(String title, String expectedUrl) { + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); + Optional url = getGoogleScholarSearchURL(entry); + assertEquals(Optional.of(expectedUrl), url); + } + + @ParameterizedTest + @CsvSource({ + "'歷史書 📖 📚', 'https://www.semanticscholar.org/search?q=%E6%AD%B7%E5%8F%B2%E6%9B%B8%20%F0%9F%93%96%20%F0%9F%93%9A'", + "' History Textbook ', 'https://www.semanticscholar.org/search?q=History%20Textbook'", + "'History%20Textbook', 'https://www.semanticscholar.org/search?q=History%2520Textbook'", + "'JabRef bibliography management', 'https://www.semanticscholar.org/search?q=JabRef%20bibliography%20management'" + }) + void getSemanticScholarSearchURLEncodesCharacters(String title, String expectedUrl) { + BibEntry entry = new BibEntry().withField(StandardField.TITLE, title); + Optional url = getSemanticScholarSearchURL(entry); + assertEquals(Optional.of(expectedUrl), url); + } + @Test void getShortScienceSearchURLReturnsEmptyOnMissingTitle() { BibEntry entry = new BibEntry(); assertEquals(Optional.empty(), getShortScienceSearchURL(entry)); } + @Test + void getGoogleScholarSearchURLReturnsEmptyOnMissingTitle() { + BibEntry entry = new BibEntry(); + assertEquals(Optional.empty(), getGoogleScholarSearchURL(entry)); + } + + @Test + void getSemanticScholarSearchURLReturnsEmptyOnMissingTitle() { + BibEntry entry = new BibEntry(); + assertEquals(Optional.empty(), getSemanticScholarSearchURL(entry)); + } + @Test void getShortScienceSearchURLWithoutLaTeX() { BibEntry entry = new BibEntry(); @@ -77,4 +137,26 @@ void getShortScienceSearchURLWithoutLaTeX() { String expectedUrl = "https://www.shortscience.org/internalsearch?q=The%20Difference%20Between%20Graph-Based%20and%20Block-Structured%20Business%20Process%20Modelling%20Languages"; assertEquals(Optional.of(expectedUrl), url); } + + @Test + void getGoogleScholarSearchURLWithoutLaTeX() { + BibEntry entry = new BibEntry(); + entry.withField(StandardField.TITLE, "{The Difference Between Graph-Based and Block-Structured Business Process Modelling Languages}"); + + Optional url = getGoogleScholarSearchURL(entry); + + String expectedUrl = "https://scholar.google.com/scholar?q=The%20Difference%20Between%20Graph-Based%20and%20Block-Structured%20Business%20Process%20Modelling%20Languages"; + assertEquals(Optional.of(expectedUrl), url); + } + + @Test + void getSemanticScholarSearchURLWithoutLaTeX() { + BibEntry entry = new BibEntry(); + entry.withField(StandardField.TITLE, "{The Difference Between Graph-Based and Block-Structured Business Process Modelling Languages}"); + + Optional url = getSemanticScholarSearchURL(entry); + + String expectedUrl = "https://www.semanticscholar.org/search?q=The%20Difference%20Between%20Graph-Based%20and%20Block-Structured%20Business%20Process%20Modelling%20Languages"; + assertEquals(Optional.of(expectedUrl), url); + } }