diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c43b77b1a..725dc8f528f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added support for selecting citation fetcher in Citations Tab. [#14430](https://github.com/JabRef/jabref/issues/14430) - In the "New Entry" dialog the identifier type is now automatically updated on typing. [#14660](https://github.com/JabRef/jabref/issues/14660) - Consistency check is now aware of custom entry types, custom fields, and reports missing required fields. [#14257](https://github.com/JabRef/jabref/pull/14257) +- We added support for [OpenCitations](https://opencitations.net/) both in the GUI (tab "Citations") and JabKit (`--get-cited-works`, `--get-citing-works`). [#14996](https://github.com/JabRef/jabref/pull/14996) - We added the ability to copy selected text from AI chat interface. [#14655](https://github.com/JabRef/jabref/issues/14655) - We added cover images for books, which will display in entry previews if available, and can be automatically downloaded when adding an entry via ISBN. [#10120](https://github.com/JabRef/jabref/issues/10120) - REST-API: Added more commands (`selectentries`, `open`, `focus`). [#14855](https://github.com/JabRef/jabref/pull/14855) @@ -44,6 +45,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue when importing an entry to a library without groups, but group "Imported Entries" was automatically created. - We fixed an issue where journal abbreviations chose the wrong abbreviation when fuzzy matching. [#14850](https://github.com/JabRef/jabref/pull/14850) - We fixed an issue where JaRef would not correctly remember the opened side panels in the preferences [#14818](https://github.com/JabRef/jabref/issues/14818) +- We fixed an issue fetching DOI information when DOIs included URL-invalid characters (e.g., `10.1002/1098-108x(198905)8:3<343::aid-eat2260080310>3.0.co;2-c`). [#14996](https://github.com/JabRef/jabref/pull/14996) - Updates of the pre-selected fetchers are now followed at the Web fetchers. [#14768](https://github.com/JabRef/jabref/pull/14768) - Restart search button in citation-relation panel now refreshes using external services. [#14757](https://github.com/JabRef/jabref/issues/14757) - Fixed groups sidebar not refreshing after importing a library. [#13684](https://github.com/JabRef/jabref/issues/13684) diff --git a/docs/glossary/references.md b/docs/glossary/references.md index b3f14a26e82..5ff49234696 100644 --- a/docs/glossary/references.md +++ b/docs/glossary/references.md @@ -9,7 +9,8 @@ parent: Glossary ## Meaning **References** is a structured list of works cited in a scientific work. -collection of bibliographic records describing scholarly works such as articles, books, or reports. + +It is usually described as the outgoing references to other cited works appearing in the reference list. In JabRef, a bibliography usually corresponds to a **.bib file** (BibTeX or BibLaTeX format) that stores all entries used for citation and reference management. diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index 8603f4cd02e..f6bfd835b05 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -394,7 +394,7 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { .withText(CitationFetcherType::getName) .install(fetcherCombo); styleTopBarNode(fetcherCombo, 75.0); - fetcherCombo.setValue(entryEditorPreferences.getCitationFetcherType()); + fetcherCombo.valueProperty().bindBidirectional(entryEditorPreferences.citationFetcherTypeProperty()); // Create abort buttons for both sides Button abortCitingButton = IconTheme.JabRefIcons.CLOSE.asButton(); @@ -457,16 +457,9 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { return; } - // Fetcher can only be changed for the citing search. - // Therefore, we handle this part only. - - // Cancel any running searches so they don't continue with the old fetcher - if (citingTask != null && !citingTask.isCancelled()) { - citingTask.cancel(); - } - entryEditorPreferences.setCitationFetcherType(newValue); - // switch the fetcher will not trigger refresh from the remote + // switch the fetcher will not trigger refresh from the remote, therefore we trigger it explicitly. searchForRelations(citingComponents, citedByComponents, false); + searchForRelations(citedByComponents, citingComponents, false); }); // Create SplitPane to hold all nodes above @@ -650,6 +643,16 @@ public boolean shouldShow(BibEntry entry) { protected void bindToEntry(BibEntry entry) { citationsRelationsTabViewModel.bindToEntry(entry); + // TODO: All this should go to ViewModel + if (citingTask != null && !citingTask.isCancelled()) { + citingTask.cancel(); + citingTask = null; + } + if (citedByTask != null && !citedByTask.isCancelled()) { + citedByTask.cancel(); + citedByTask = null; + } + SplitPane splitPane = getPaneAndStartSearch(entry); splitPane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); splitPane.setMinSize(0, 0); @@ -728,7 +731,6 @@ private void executeSearch(CitationComponents citationComponents, boolean bypass ObservableList observableList = FXCollections.observableArrayList(); citationComponents.listView().setItems(observableList); - // TODO: It should not be possible to cancel a search task that is already running for same tab if (citationComponents.searchType() == CitationFetcher.SearchType.CITES && citingTask != null && !citingTask.isCancelled()) { citingTask.cancel(); } else if (citationComponents.searchType() == CitationFetcher.SearchType.CITED_BY && citedByTask != null && !citedByTask.isCancelled()) { @@ -765,13 +767,13 @@ private BackgroundTask> createBackgroundTask( ) { return switch (searchType) { case CitationFetcher.SearchType.CITES -> { - citingTask = BackgroundTask.wrap( + this.citingTask = BackgroundTask.wrap( () -> this.searchCitationsRelationsService.searchCites(entry, bypassCache) ); yield citingTask; } case CitationFetcher.SearchType.CITED_BY -> { - citedByTask = BackgroundTask.wrap( + this.citedByTask = BackgroundTask.wrap( () -> this.searchCitationsRelationsService.searchCitedBy(entry, bypassCache) ); yield citedByTask; diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java b/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java index 4a7fed4fc55..c92871dd96c 100644 --- a/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java @@ -35,7 +35,7 @@ class GetCitedWorks implements Callable { converter = CitationFetcherTypeConverter.class, description = "Metadata provider: ${COMPLETION-CANDIDATES}" ) - private CitationFetcherType citationFetcherType = CitationFetcherType.CROSSREF; + private CitationFetcherType citationFetcherType = CitationFetcherType.OPEN_CITATIONS; @CommandLine.Parameters(description = "DOI to check") private String doi; @@ -50,15 +50,14 @@ public Integer call() { LOGGER::info, new CurrentThreadTaskExecutor()); - CitationFetcher citationFetcher = CitationFetcherType - .getCitationFetcher( - citationFetcherType, - preferences.getImporterPreferences(), - preferences.getImportFormatPreferences(), - preferences.getCitationKeyPatternPreferences(), - preferences.getGrobidPreferences(), - aiService - ); + CitationFetcher citationFetcher = CitationFetcherType.getCitationFetcher( + citationFetcherType, + preferences.getImporterPreferences(), + preferences.getImportFormatPreferences(), + preferences.getCitationKeyPatternPreferences(), + preferences.getGrobidPreferences(), + aiService + ); List entries; diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java b/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java index 505af4be121..d98c7ea4473 100644 --- a/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java @@ -3,12 +3,16 @@ import java.util.List; import java.util.concurrent.Callable; +import org.jabref.logic.ai.AiService; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.fetcher.citation.CitationFetcher; -import org.jabref.logic.importer.fetcher.citation.semanticscholar.SemanticScholarCitationFetcher; +import org.jabref.logic.importer.fetcher.citation.CitationFetcherType; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.util.CurrentThreadTaskExecutor; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.toolkit.converter.CitationFetcherTypeConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,12 +30,34 @@ class GetCitingWorks implements Callable { @CommandLine.Mixin private JabKit.SharedOptions sharedOptions = new JabKit.SharedOptions(); + @CommandLine.Option( + names = "--provider", + converter = CitationFetcherTypeConverter.class, + description = "Metadata provider: ${COMPLETION-CANDIDATES}" + ) + private CitationFetcherType citationFetcherType = CitationFetcherType.OPEN_CITATIONS; + @CommandLine.Parameters(description = "DOI to check") private String doi; @Override public Integer call() { - CitationFetcher citationFetcher = new SemanticScholarCitationFetcher(argumentProcessor.cliPreferences.getImporterPreferences()); + CliPreferences preferences = argumentProcessor.cliPreferences; + AiService aiService = new AiService( + preferences.getAiPreferences(), + preferences.getFilePreferences(), + preferences.getCitationKeyPatternPreferences(), + LOGGER::info, + new CurrentThreadTaskExecutor()); + + CitationFetcher citationFetcher = CitationFetcherType.getCitationFetcher( + citationFetcherType, + preferences.getImporterPreferences(), + preferences.getImportFormatPreferences(), + preferences.getCitationKeyPatternPreferences(), + preferences.getGrobidPreferences(), + aiService + ); List entries; diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/CrossRef.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/CrossRef.java index d5c4a1c75fc..f4f1a02e939 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/CrossRef.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/CrossRef.java @@ -3,6 +3,8 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -63,8 +65,8 @@ public URL getURLForEntry(BibEntry entry) throws URISyntaxException, MalformedUR entry.getFieldLatexFree(StandardField.YEAR).ifPresent(year -> uriBuilder.addParameter("filter", "from-pub-date:" + year) ); - uriBuilder.addParameter("rows", "20"); // = API default - uriBuilder.addParameter("offset", "0"); // start at beginning + uriBuilder.addParameter("rows", "20"); // = API default + uriBuilder.addParameter("offset", "0"); // start at the beginning return uriBuilder.build().toURL(); } @@ -77,7 +79,7 @@ public URL getURLForQuery(BaseQueryNode queryNode) throws URISyntaxException, Ma @Override public URL getUrlForIdentifier(String identifier) throws URISyntaxException, MalformedURLException { - URIBuilder uriBuilder = new URIBuilder(API_URL + "/" + identifier); + URIBuilder uriBuilder = new URIBuilder(API_URL + "/" + URLEncoder.encode(identifier, StandardCharsets.UTF_8)); return uriBuilder.build().toURL(); } diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java index d304f78e9cb..4d42e921965 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java @@ -5,12 +5,14 @@ import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.importer.fetcher.citation.crossref.CrossRefCitationFetcher; +import org.jabref.logic.importer.fetcher.citation.opencitations.OpenCitationsFetcher; import org.jabref.logic.importer.fetcher.citation.semanticscholar.SemanticScholarCitationFetcher; import org.jabref.logic.importer.util.GrobidPreferences; public enum CitationFetcherType { - CROSSREF("CrossRef"), - SEMANTIC_SCHOLAR("Semantic Scholar"); + CROSSREF(CrossRefCitationFetcher.FETCHER_NAME), + OPEN_CITATIONS(OpenCitationsFetcher.FETCHER_NAME), + SEMANTIC_SCHOLAR(SemanticScholarCitationFetcher.FETCHER_NAME); private final String name; @@ -37,6 +39,8 @@ public static CitationFetcher getCitationFetcher(CitationFetcherType citationFet case CROSSREF -> new CrossRefCitationFetcher(importerPreferences, importFormatPreferences, citationKeyPatternPreferences, grobidPreferences, aiService); + case OPEN_CITATIONS -> + new OpenCitationsFetcher(importerPreferences); case SEMANTIC_SCHOLAR -> new SemanticScholarCitationFetcher(importerPreferences); }; diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationItem.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationItem.java new file mode 100644 index 00000000000..8bafd547f91 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationItem.java @@ -0,0 +1,60 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; + +import com.google.gson.annotations.SerializedName; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class CitationItem { + @Nullable String oci; + @Nullable String citing; + @Nullable String cited; + @Nullable String creation; + @Nullable String timespan; + + @SerializedName("journal_sc") + @Nullable String journalSelfCitation; + + @SerializedName("author_sc") + @Nullable String authorSelfCitation; + + record IdentifierWithField(Field field, String value) { + } + + List extractIdentifiers(@Nullable String pidString) { + if (pidString == null || pidString.isEmpty()) { + return List.of(); + } + + String[] pids = pidString.split("\\s+"); + List identifiers = new ArrayList<>(); + for (String pid : pids) { + int colonIndex = pid.indexOf(':'); + if (colonIndex > 0) { + String prefix = pid.substring(0, colonIndex); + String value = pid.substring(colonIndex + 1); + Field field = FieldFactory.parseField(prefix); + identifiers.add(new IdentifierWithField(field, value)); + } else { + identifiers.add(new IdentifierWithField(StandardField.NOTE, pid)); + } + } + + return identifiers; + } + + List citingIdentifiers() { + return extractIdentifiers(citing); + } + + List citedIdentifiers() { + return extractIdentifiers(cited); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CountResponse.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CountResponse.java new file mode 100644 index 00000000000..ef18ee0145b --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CountResponse.java @@ -0,0 +1,20 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class CountResponse { + @Nullable String count; + + int countAsInt() { + if (count == null) { + return 0; + } + try { + return Integer.parseInt(count); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java new file mode 100644 index 00000000000..7c8332db8c8 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java @@ -0,0 +1,162 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.ImporterPreferences; +import org.jabref.logic.importer.fetcher.CrossRef; +import org.jabref.logic.importer.fetcher.citation.CitationFetcher; +import org.jabref.logic.net.URLDownload; +import org.jabref.logic.util.URLUtil; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.identifier.DOI; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.jspecify.annotations.NullMarked; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NullMarked +public class OpenCitationsFetcher implements CitationFetcher { + public static final String FETCHER_NAME = "OpenCitations"; + + private static final Logger LOGGER = LoggerFactory.getLogger(OpenCitationsFetcher.class); + private static final String API_BASE_URL = "https://api.opencitations.net/index/v2"; + private static final Gson GSON = new Gson(); + + private final ImporterPreferences importerPreferences; + private final CrossRef crossRefFetcher = new CrossRef(); + + public OpenCitationsFetcher(ImporterPreferences importerPreferences) { + this.importerPreferences = importerPreferences; + } + + @Override + public String getName() { + return FETCHER_NAME; + } + + private String getApiUrl(String endpoint, BibEntry entry) throws FetcherException { + String doi = entry.getDOI() + .orElseThrow(() -> new FetcherException("Entry does not have a DOI")) + .asString(); + return API_BASE_URL + "/" + endpoint + "/doi:" + doi; + } + + @Override + public List getReferences(BibEntry entry) throws FetcherException { + return fetchCitationData(entry, "references", CitationItem::citedIdentifiers); + } + + @Override + public List getCitations(BibEntry entry) throws FetcherException { + return fetchCitationData(entry, "citations", CitationItem::citingIdentifiers); + } + + /// API explained at and + private List fetchCitationData(BibEntry entry, String endpoint, Function> identifierExtractor) throws FetcherException { + Optional doi = entry.getDOI(); + if (doi.isEmpty()) { + return List.of(); + } + + String apiUrl = getApiUrl(endpoint, entry); + LOGGER.debug("{} URL: {}", endpoint, apiUrl); + + try { + URL url = URLUtil.create(apiUrl); + URLDownload urlDownload = new URLDownload(importerPreferences, url); + importerPreferences.getApiKey(getName()) + .ifPresent(apiKey -> urlDownload.addHeader("authorization", apiKey)); + + String jsonResponse = urlDownload.asString(); + CitationItem[] citationItems = GSON.fromJson(jsonResponse, CitationItem[].class); + + if (citationItems == null || citationItems.length == 0) { + return List.of(); + } + + List entries = new ArrayList<>(); + for (CitationItem item : citationItems) { + List identifiers = identifierExtractor.apply(item); + if (!identifiers.isEmpty()) { + entries.add(fetchBibEntryFromIdentifiers(identifiers)); + } + } + + return entries; + } catch (MalformedURLException e) { + throw new FetcherException("Malformed URL", e); + } catch (JsonSyntaxException e) { + throw new FetcherException("Could not parse JSON response from OpenCitations", e); + } + } + + private BibEntry fetchBibEntryFromIdentifiers(List identifiers) { + Optional doiIdentifier = identifiers.stream() + .filter(id -> id.field().equals(StandardField.DOI)) + .findFirst(); + + if (doiIdentifier.isPresent()) { + try { + Optional fetchedEntry = crossRefFetcher.performSearchById(doiIdentifier.get().value()); + if (fetchedEntry.isPresent()) { + BibEntry entry = fetchedEntry.get(); + for (CitationItem.IdentifierWithField identifier : identifiers) { + if (!entry.hasField(identifier.field())) { + entry.setField(identifier.field(), identifier.value()); + } + } + return entry; + } + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiIdentifier.get().value(), e); + } + } + + BibEntry bibEntry = new BibEntry(); + for (CitationItem.IdentifierWithField identifier : identifiers) { + bibEntry.setField(identifier.field(), identifier.value()); + } + return bibEntry; + } + + /// API explained at + @Override + public Optional getCitationCount(BibEntry entry) throws FetcherException { + if (entry.getDOI().isEmpty()) { + return Optional.empty(); + } + + String apiUrl = getApiUrl("citation-count", entry); + LOGGER.debug("Citation count URL: {}", apiUrl); + + try { + URL url = URLUtil.create(apiUrl); + URLDownload urlDownload = new URLDownload(importerPreferences, url); + importerPreferences.getApiKey(getName()) + .ifPresent(apiKey -> urlDownload.addHeader("authorization", apiKey)); + + String jsonResponse = urlDownload.asString(); + CountResponse countResponse = GSON.fromJson(jsonResponse, CountResponse.class); + + if (countResponse == null) { + return Optional.empty(); + } + + int count = countResponse.countAsInt(); + return count > 0 ? Optional.of(count) : Optional.empty(); + } catch (MalformedURLException e) { + throw new FetcherException("Malformed URL", e); + } catch (JsonSyntaxException e) { + throw new FetcherException("Could not parse JSON response from OpenCitations", e); + } + } +} diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/semanticscholar/SemanticScholarCitationFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/semanticscholar/SemanticScholarCitationFetcher.java index 6755795a268..1668a7853c3 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/semanticscholar/SemanticScholarCitationFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/semanticscholar/SemanticScholarCitationFetcher.java @@ -17,12 +17,13 @@ import com.google.gson.Gson; import kong.unirest.core.json.JSONObject; import org.jooq.lambda.Unchecked; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@NullMarked public class SemanticScholarCitationFetcher implements CitationFetcher, CustomizableKeyFetcher { - public static final String FETCHER_NAME = "Semantic Scholar Citations Fetcher"; + public static final String FETCHER_NAME = "Semantic Scholar"; private static final Logger LOGGER = LoggerFactory.getLogger(SemanticScholarCitationFetcher.class); @@ -42,14 +43,14 @@ public String getAPIUrl(String entryPoint, BibEntry entry) { + "&limit=1000"; } - public @NonNull String getUrlForCitationCount(@NonNull BibEntry entry) { + public String getUrlForCitationCount(BibEntry entry) { return SEMANTIC_SCHOLAR_API + "paper/" + "DOI:" + entry.getDOI().orElseThrow().asString() + "?fields=" + "citationCount" + "&limit=1"; } @Override - public @NonNull List<@NonNull BibEntry> getCitations(@NonNull BibEntry entry) throws FetcherException { + public List getCitations(BibEntry entry) throws FetcherException { if (entry.getDOI().isEmpty()) { return List.of(); } @@ -74,7 +75,7 @@ public String getAPIUrl(String entryPoint, BibEntry entry) { } @Override - public @NonNull List<@NonNull BibEntry> getReferences(@NonNull BibEntry entry) throws FetcherException { + public List getReferences(BibEntry entry) throws FetcherException { if (entry.getDOI().isEmpty()) { return List.of(); }