Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f572b32
Refine explanation of references
koppor Feb 1, 2026
4e5f01e
Begin of OpenCitationsFetcher
koppor Feb 1, 2026
b31873f
Initialize task: New task
koppor Feb 1, 2026
4d7d56c
Create Response Model Classes
koppor Feb 1, 2026
0c62740
Technical Specification
koppor Feb 1, 2026
312562b
Technical Specification
koppor Feb 1, 2026
d6545b1
Create Response Model Classes
koppor Feb 1, 2026
6004e27
Create Response Model Classes
koppor Feb 1, 2026
d3e6beb
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
345bac0
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
4e64795
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
5406e4f
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
b13ffec
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
f3867e1
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
057abaf
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
5f77b88
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
84fd50f
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
3538b3d
Implement OpenCitationsFetcher Core Class
koppor Feb 1, 2026
8a488ec
Register OpenCitations in CitationFetcherType Enum
koppor Feb 1, 2026
51bbae3
Merge branch 'new-task-ed21' into add-open-citations
koppor Feb 1, 2026
51dd141
Remove zenflow things
koppor Feb 1, 2026
b6a7cc9
Merge remote-tracking branch 'origin/main' into add-open-citations
koppor Feb 1, 2026
3979fdc
JabKit: Use OpenCitations instead of CrossRef
koppor Feb 1, 2026
2f811ee
Add selection of citation providers also for GetCitingWorks
koppor Feb 1, 2026
3454654
Begin to wire OPEN_CITATIONS into GUI
koppor Feb 1, 2026
343b6f5
Refine code
koppor Feb 1, 2026
15a1537
Wire OpenCitations into GUI
koppor Feb 1, 2026
6bff9a9
Add CHANGELOG.md entry
koppor Feb 1, 2026
8869a0b
Fix CrossRef fetching issue
koppor Feb 1, 2026
dd3dabc
Add link to CHANGELOG.md
koppor Feb 1, 2026
64e5d27
Add links to API - and group methods more together
koppor Feb 1, 2026
75e377e
Fix references.md
koppor Feb 1, 2026
dcd6c31
Fix format
koppor Feb 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion docs/glossary/references.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why bidirectional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Common pattern in JabRef's code base
  2. It might be that some other UI element will allow for changing the citatoin fetcher. E.g., when checking citation counts at org.jabref.gui.fieldeditors.CitationCountEditor


// Create abort buttons for both sides
Button abortCitingButton = IconTheme.JabRefIcons.CLOSE.asButton();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -650,6 +643,16 @@ public boolean shouldShow(BibEntry entry) {
protected void bindToEntry(BibEntry entry) {
citationsRelationsTabViewModel.bindToEntry(entry);

// TODO: All this should go to ViewModel
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you do this or for follow-up

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe me, maybe someone else. This was in before - I created an issue for this.

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);
Expand Down Expand Up @@ -728,7 +731,6 @@ private void executeSearch(CitationComponents citationComponents, boolean bypass
ObservableList<CitationRelationItem> 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()) {
Expand Down Expand Up @@ -765,13 +767,13 @@ private BackgroundTask<List<BibEntry>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class GetCitedWorks implements Callable<Integer> {
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;
Expand All @@ -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<BibEntry> entries;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,12 +30,34 @@ class GetCitingWorks implements Callable<Integer> {
@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<BibEntry> entries;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IdentifierWithField> extractIdentifiers(@Nullable String pidString) {
if (pidString == null || pidString.isEmpty()) {
return List.of();
}

String[] pids = pidString.split("\\s+");
List<IdentifierWithField> 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<IdentifierWithField> citingIdentifiers() {
return extractIdentifiers(citing);
}

List<IdentifierWithField> citedIdentifiers() {
return extractIdentifiers(cited);
}
}
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this class here really necessary?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO pattern. Better consistency in always using a DTO in this class than case-by-case

--> CitatonItem is the other DTO.

} catch (NumberFormatException e) {
return 0;
}
}
}
Loading