Skip to content

Commit 0bda89c

Browse files
committed
feat(external-search): add Semantic Scholar search
1 parent 4d3484e commit 0bda89c

File tree

12 files changed

+173
-14
lines changed

12 files changed

+173
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
2424
- We added the possibility to configure the email provided to unpaywall. [#14340](https://github.com/JabRef/jabref/pull/14340)
2525
- We added a "Regenerate" button for the AI chat allowing the user to make the language model reformulate its response to the previous prompt. [#12191](https://github.com/JabRef/jabref/issues/12191)
2626
- We added support for transliteration of fields to English and automatic transliteration of generated citation key. [#11377](https://github.com/JabRef/jabref/issues/11377)
27-
- 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)
27+
- We added support for "Search Google Scholar" and "Search Semantic Scholar" to quickly search for a selected entry's title in Google Scholar or Semantic Scholar directly from the main table's context menu [#12268](https://github.com/JabRef/jabref/issues/12268)
2828
- 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)
2929

3030
### Changed

build-logic/java-module-packaging

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit a9efaeed93805124226c195635568a69a0d56eb0

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public enum StandardActions implements Action {
4545
EXTRACT_FILE_REFERENCES_OFFLINE(Localization.lang("Extract references from file (offline)"), IconTheme.JabRefIcons.FILE_STAR),
4646
OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI),
4747
SEARCH_GOOGLE_SCHOLAR(Localization.lang("Search Google Scholar")),
48+
SEARCH_SEMANTIC_SCHOLAR(Localization.lang("Search Semantic Scholar")),
4849
SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")),
4950
SEARCH(Localization.lang("Search...")),
5051
MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/..."), KeyBinding.MERGE_WITH_FETCHED_ENTRY),

jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ private static Menu createSearchSubMenu(ActionFactory factory,
246246
Menu searchMenu = factory.createMenu(StandardActions.SEARCH);
247247
searchMenu.getItems().addAll(
248248
factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences)),
249+
factory.createMenuItem(StandardActions.SEARCH_SEMANTIC_SCHOLAR, new SearchSemanticScholarAction(dialogService, stateManager, preferences)),
249250
factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences))
250251
);
251252
return searchMenu;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.jabref.gui.maintable;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
6+
import javafx.beans.binding.BooleanExpression;
7+
8+
import org.jabref.gui.DialogService;
9+
import org.jabref.gui.StateManager;
10+
import org.jabref.gui.actions.SimpleCommand;
11+
import org.jabref.gui.desktop.os.NativeDesktop;
12+
import org.jabref.gui.preferences.GuiPreferences;
13+
import org.jabref.logic.l10n.Localization;
14+
import org.jabref.logic.util.ExternalLinkCreator;
15+
import org.jabref.model.entry.BibEntry;
16+
import org.jabref.model.entry.field.StandardField;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry;
22+
import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected;
23+
24+
public class SearchSemanticScholarAction extends SimpleCommand {
25+
private static final Logger LOGGER = LoggerFactory.getLogger(SearchSemanticScholarAction .class);
26+
27+
private final DialogService dialogService;
28+
private final StateManager stateManager;
29+
private final GuiPreferences preferences;
30+
private final ExternalLinkCreator externalLinkCreator;
31+
32+
public SearchSemanticScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) {
33+
this.dialogService = dialogService;
34+
this.stateManager = stateManager;
35+
this.preferences = preferences;
36+
37+
this.externalLinkCreator = new ExternalLinkCreator(preferences.getImporterPreferences());
38+
39+
BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager);
40+
this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet));
41+
}
42+
43+
@Override
44+
public void execute() {
45+
stateManager.getActiveDatabase().ifPresent(databaseContext -> {
46+
final List<BibEntry> bibEntries = stateManager.getSelectedEntries();
47+
externalLinkCreator.getSemanticScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> {
48+
try {
49+
NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst());
50+
} catch (IOException ex) {
51+
LOGGER.warn("Could not open Semantic Scholar", ex);
52+
dialogService.notify(Localization.lang("Unable to open Semantic Scholar.") + " " + ex.getMessage());
53+
}
54+
});
55+
});
56+
}
57+
}

jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ private void setupSearchEngines() {
142142
// add default search engines
143143
searchEngines.addAll(
144144
new SearchEngineItem("Google Scholar", "https://scholar.google.com/scholar?q={title}"),
145+
new SearchEngineItem("Semantic Scholar", "https://www.semanticscholar.org/search?q={title}"),
145146
new SearchEngineItem("Short Science", "https://www.shortscience.org/internalsearch?q={title}")
146147
);
147148
}

jablib/src/main/java/org/jabref/logic/util/ExternalLinkCreator.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class ExternalLinkCreator {
2020
private static final Logger LOGGER = LoggerFactory.getLogger(ExternalLinkCreator.class);
2121

2222
private static final String DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL = "https://scholar.google.com/scholar";
23+
private static final String DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL = "https://www.semanticscholar.org/search";
2324
private static final String DEFAULT_SHORTSCIENCE_SEARCH_URL = "https://www.shortscience.org/internalsearch";
2425

2526
private final ImporterPreferences importerPreferences;
@@ -41,7 +42,22 @@ public Optional<String> getGoogleScholarSearchURL(BibEntry entry) {
4142
String baseUrl = importerPreferences.getSearchEngineUrlTemplates()
4243
.getOrDefault("Google Scholar", DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL);
4344
String author = entry.getField(StandardField.AUTHOR).orElse(null);
44-
return buildSearchUrl(baseUrl, DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL, title, author, "Google Scholar");
45+
return buildSearchUrl(baseUrl, DEFAULT_GOOGLE_SCHOLAR_SEARCH_URL, title, author, "Google Scholar", false);
46+
});
47+
}
48+
49+
/**
50+
* Get a URL to the search results of Semantic Scholar for the BibEntry's title
51+
*
52+
* @param entry The entry to search for. Expects the BibEntry's title to be set for successful return.
53+
* @return The URL if it was successfully created
54+
*/
55+
public Optional<String> getSemanticScholarSearchURL(BibEntry entry) {
56+
return entry.getField(StandardField.TITLE).flatMap(title -> {
57+
String baseUrl = importerPreferences.getSearchEngineUrlTemplates()
58+
.getOrDefault("Semantic Scholar", DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL);
59+
String author = entry.getField(StandardField.AUTHOR).orElse(null);
60+
return buildSearchUrl(baseUrl, DEFAULT_SEMANTIC_SCHOLAR_SEARCH_URL, title, author, "Semantic Scholar", true);
4561
});
4662
}
4763

@@ -56,7 +72,7 @@ public Optional<String> getShortScienceSearchURL(BibEntry entry) {
5672
String baseUrl = importerPreferences.getSearchEngineUrlTemplates()
5773
.getOrDefault("Short Science", DEFAULT_SHORTSCIENCE_SEARCH_URL);
5874
String author = entry.getField(StandardField.AUTHOR).orElse(null);
59-
return buildSearchUrl(baseUrl, DEFAULT_SHORTSCIENCE_SEARCH_URL, title, author, "ShortScience");
75+
return buildSearchUrl(baseUrl, DEFAULT_SHORTSCIENCE_SEARCH_URL, title, author, "ShortScience", false);
6076
});
6177
}
6278

@@ -70,7 +86,7 @@ public Optional<String> getShortScienceSearchURL(BibEntry entry) {
7086
* @param serviceName Name of the service for logging
7187
* @return Optional containing the constructed URL, or empty if construction failed
7288
*/
73-
private Optional<String> buildSearchUrl(String baseUrl, String defaultUrl, String title, @Nullable String author, String serviceName) {
89+
private Optional<String> buildSearchUrl(String baseUrl, String defaultUrl, String title, @Nullable String author, String serviceName, boolean addAuthorIndex) {
7490
// Converting LaTeX-formatted titles (e.g., containing braces) to plain Unicode to ensure compatibility with ShortScience's search URL.
7591
// LatexToUnicodeAdapter.format() is being used because it attempts to parse LaTeX, but gracefully degrades to a normalized title on failure.
7692
// This avoids sending malformed or literal LaTeX syntax titles that would give the wrong result.
@@ -81,13 +97,13 @@ private Optional<String> buildSearchUrl(String baseUrl, String defaultUrl, Strin
8197
String lowerUrl = baseUrl.toLowerCase().trim();
8298
if (StringUtil.isBlank(lowerUrl) || !(lowerUrl.startsWith("http://") || lowerUrl.startsWith("https://"))) {
8399
LOGGER.warn("Invalid URL scheme in {} preference: {}. Using default URL.", serviceName, baseUrl);
84-
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName);
100+
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName, addAuthorIndex);
85101
}
86102

87103
// If URL doesn't contain {title}, it's not a valid template, use query parameters
88104
if (!baseUrl.contains("{title}")) {
89105
LOGGER.warn("URL template for {} doesn't contain {{title}} placeholder. Using query parameters.", serviceName);
90-
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName);
106+
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName, addAuthorIndex);
91107
}
92108

93109
// Replace placeholders with URL-encoded values
@@ -109,24 +125,30 @@ private Optional<String> buildSearchUrl(String baseUrl, String defaultUrl, Strin
109125
return Optional.of(finalUrl);
110126
} else {
111127
LOGGER.warn("Constructed URL for {} is invalid: {}. Using default URL.", serviceName, finalUrl);
112-
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName);
128+
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName, addAuthorIndex);
113129
}
114130
} catch (Exception ex) {
115131
LOGGER.error("Error constructing URL for {}: {}", serviceName, ex.getMessage(), ex);
116-
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName);
132+
return buildUrlWithQueryParams(defaultUrl, filteredTitle, author, serviceName, addAuthorIndex);
117133
}
118134
}
119135

120136
/**
121-
* Builds a URL using query parameters (fallback method)
137+
* Builds a URL using query parameters (fallback method).
138+
* <p>
139+
* The parameter addAuthorIndex is used for Semantic Scholar service because it does not understand "author=XYZ", but it uses "author[0]=XYZ&author[1]=ABC".
122140
*/
123-
private Optional<String> buildUrlWithQueryParams(String baseUrl, String title, @Nullable String author, String serviceName) {
141+
private Optional<String> buildUrlWithQueryParams(String baseUrl, String title, @Nullable String author, String serviceName, boolean addAuthorIndex) {
124142
try {
125143
URIBuilder uriBuilder = new URIBuilder(baseUrl);
126144
// Title is already converted to Unicode by buildSearchUrl before reaching here
127145
uriBuilder.addParameter("q", title.trim());
128146
if (author != null) {
129-
uriBuilder.addParameter("author", author.trim());
147+
if (addAuthorIndex) {
148+
uriBuilder.addParameter("author[0]", author.trim());
149+
} else {
150+
uriBuilder.addParameter("author", author.trim());
151+
}
130152
}
131153
return Optional.of(uriBuilder.toString());
132154
} catch (URISyntaxException ex) {
Submodule csl-locales updated 67 files
Submodule csl-styles updated 114 files

0 commit comments

Comments
 (0)