From f572b3223d4914242d2ea096738658a482fb9bec Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:06:44 +0100 Subject: [PATCH 01/31] Refine explanation of references --- docs/glossary/references.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/glossary/references.md b/docs/glossary/references.md index b3f14a26e82..7c1469e841a 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 or the In JabRef, a bibliography usually corresponds to a **.bib file** (BibTeX or BibLaTeX format) that stores all entries used for citation and reference management. From 4e5f01ee3f80fec31812c88b990ea755bc10c257 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:08:43 +0100 Subject: [PATCH 02/31] Begin of OpenCitationsFetcher --- .../opencitations/OpenCitationsFetcher.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java 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..d38bca0d3d0 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java @@ -0,0 +1,13 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import org.jabref.logic.importer.fetcher.citation.CitationFetcher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenCitationsFetcher implements CitationFetcher { + + public static final String FETCHER_NAME = "OpenCitations"; + + private static final Logger LOGGER = LoggerFactory.getLogger(OpenCitationsFetcher.class); +} From b31873f0e1250c94702de124bf2182cb350e1279 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:29:52 +0100 Subject: [PATCH 03/31] Initialize task: New task --- .zenflow/tasks/new-task-ed21/plan.md | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .zenflow/tasks/new-task-ed21/plan.md diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md new file mode 100644 index 00000000000..81198350a14 --- /dev/null +++ b/.zenflow/tasks/new-task-ed21/plan.md @@ -0,0 +1,64 @@ +# Spec and build + +## Configuration +- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}` + +--- + +## Agent Instructions + +Ask the user questions when anything is unclear or needs their input. This includes: +- Ambiguous or incomplete requirements +- Technical decisions that affect architecture or user experience +- Trade-offs that require business context + +Do not make assumptions on important decisions — get clarification first. + +--- + +## Workflow Steps + +### [ ] Step: Technical Specification + +Assess the task's difficulty, as underestimating it leads to poor outcomes. +- easy: Straightforward implementation, trivial bug fix or feature +- medium: Moderate complexity, some edge cases or caveats to consider +- hard: Complex logic, many caveats, architectural considerations, or high-risk changes + +Create a technical specification for the task that is appropriate for the complexity level: +- Review the existing codebase architecture and identify reusable components. +- Define the implementation approach based on established patterns in the project. +- Identify all source code files that will be created or modified. +- Define any necessary data model, API, or interface changes. +- Describe verification steps using the project's test and lint commands. + +Save the output to `{@artifacts_path}/spec.md` with: +- Technical context (language, dependencies) +- Implementation approach +- Source code structure changes +- Data model / API / interface changes +- Verification approach + +If the task is complex enough, create a detailed implementation plan based on `{@artifacts_path}/spec.md`: +- Break down the work into concrete tasks (incrementable, testable milestones) +- Each task should reference relevant contracts and include verification steps +- Replace the Implementation step below with the planned tasks + +Rule of thumb for step size: each step should represent a coherent unit of work (e.g., implement a component, add an API endpoint, write tests for a module). Avoid steps that are too granular (single function). + +Save to `{@artifacts_path}/plan.md`. If the feature is trivial and doesn't warrant this breakdown, keep the Implementation step below as is. + +--- + +### [ ] Step: Implementation + +Implement the task according to the technical specification and general engineering best practices. + +1. Break the task into steps where possible. +2. Implement the required changes in the codebase. +3. Add and run relevant tests and linters. +4. Perform basic manual verification if applicable. +5. After completion, write a report to `{@artifacts_path}/report.md` describing: + - What was implemented + - How the solution was tested + - The biggest issues or challenges encountered From 4d7d56c1d3cbb2d78f2ac2b4558a6a161504d44e Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:35:58 +0100 Subject: [PATCH 04/31] Create Response Model Classes Now let me check the directory structure and create the model classes: --- .zenflow/tasks/new-task-ed21/plan.md | 80 ++++- .zenflow/tasks/new-task-ed21/spec.md | 318 ++++++++++++++++++ .../citation/opencitations/CitationItem.java | 97 ++++++ .../opencitations/CitationResponse.java | 23 ++ .../citation/opencitations/CountResponse.java | 21 ++ 5 files changed, 528 insertions(+), 11 deletions(-) create mode 100644 .zenflow/tasks/new-task-ed21/spec.md create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationItem.java create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CountResponse.java diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md index 81198350a14..b7355e50e94 100644 --- a/.zenflow/tasks/new-task-ed21/plan.md +++ b/.zenflow/tasks/new-task-ed21/plan.md @@ -18,7 +18,8 @@ Do not make assumptions on important decisions — get clarification first. ## Workflow Steps -### [ ] Step: Technical Specification +### [x] Step: Technical Specification + Assess the task's difficulty, as underestimating it leads to poor outcomes. - easy: Straightforward implementation, trivial bug fix or feature @@ -50,15 +51,72 @@ Save to `{@artifacts_path}/plan.md`. If the feature is trivial and doesn't warra --- -### [ ] Step: Implementation +### [x] Step: Create Response Model Classes + -Implement the task according to the technical specification and general engineering best practices. +Create the data model classes for OpenCitations API responses: +- `CitationItem.java`: Individual citation record with OCI, citing/cited PIDs, creation date, timespan +- `CitationResponse.java`: Array wrapper for citations/references +- `CountResponse.java`: Wrapper for count responses -1. Break the task into steps where possible. -2. Implement the required changes in the codebase. -3. Add and run relevant tests and linters. -4. Perform basic manual verification if applicable. -5. After completion, write a report to `{@artifacts_path}/report.md` describing: - - What was implemented - - How the solution was tested - - The biggest issues or challenges encountered +Each class should: +- Include Gson annotations where needed (e.g., @SerializedName) +- Provide getters/setters +- Add helper method in CitationItem to extract DOI from PID string + +**Verification**: Models should compile without errors and follow JabRef code style. + +--- + +### [ ] Step: Implement OpenCitationsFetcher Core Class + +Implement `OpenCitationsFetcher` class that implements `CitationFetcher` interface: +- Constructor accepting `ImporterPreferences` +- `getName()` returning "OpenCitations" +- URL construction methods for references, citations, and counts +- Implement `getReferences()`: fetch outgoing references +- Implement `getCitations()`: fetch incoming citations +- Implement `getCitationCount()`: fetch citation count +- DOI extraction and BibEntry conversion logic +- Error handling for missing DOI, network errors, malformed responses +- Optional API key support via ImporterPreferences + +**Verification**: Class compiles, implements all required interface methods. + +--- + +### [ ] Step: Register OpenCitations in CitationFetcherType Enum + +Update `CitationFetcherType.java`: +- Add `OPENCITATIONS("OpenCitations")` enum constant +- Add case in `getCitationFetcher()` switch to instantiate `OpenCitationsFetcher` + +**Verification**: Enum compiles, factory method returns correct fetcher instance. + +--- + +### [ ] Step: Implement Unit Tests + +Create `OpenCitationsFetcherTest.java` with test cases: +- `testGetReferencesWithValidDoi()`: Test reference fetching with known DOI +- `testGetCitationsWithValidDoi()`: Test citation fetching with known DOI +- `testGetCitationCount()`: Verify citation count retrieval +- `testEmptyWhenNoDoi()`: Verify empty list when DOI missing +- `testGetName()`: Verify fetcher name + +Mark test class with `@FetcherTest` annotation. + +**Verification**: Run `./gradlew :jablib:test --tests "*OpenCitations*"` - all tests pass. + +--- + +### [ ] Step: Final Verification and Reporting + +1. Run full test suite for citation fetchers +2. Verify code style compliance (if checkstyle configured) +3. Manual smoke test with JabRef (if applicable) +4. Write completion report to `{@artifacts_path}/report.md` with: + - Summary of what was implemented + - Test results + - Any challenges or issues encountered + - Known limitations (e.g., DOI-only support) diff --git a/.zenflow/tasks/new-task-ed21/spec.md b/.zenflow/tasks/new-task-ed21/spec.md new file mode 100644 index 00000000000..0d92d054bcb --- /dev/null +++ b/.zenflow/tasks/new-task-ed21/spec.md @@ -0,0 +1,318 @@ +# Technical Specification: OpenCitations Citation Fetcher + +## Task Difficulty Assessment + +**Difficulty Level: Medium** + +**Rationale:** +- The implementation follows established patterns in the codebase (SemanticScholarCitationFetcher, CrossRefCitationFetcher) +- OpenCitations API is well-documented with a straightforward REST API structure +- Response parsing is simple JSON (no complex nested structures) +- No authentication required (API key is optional for rate limit increase) +- Some edge cases need handling (DOI extraction from PIDs, empty responses) +- Requires creating response model classes and proper error handling + +--- + +## Technical Context + +### Language & Dependencies +- **Language**: Java 21+ +- **Key Dependencies**: + - Gson (JSON parsing) - already used in SemanticScholarCitationFetcher + - Jackson (alternative, used in CrossRefCitationFetcher) + - JUnit 5 (testing) + - Mockito (mocking in tests) + - JabRef's existing fetcher infrastructure + +### API Details +- **Base URL**: `https://api.opencitations.net/index/v2` +- **Key Endpoints**: + - `/references/{id}`: Get outgoing references from a paper (supports DOI, PMID, OMID) + - `/citations/{id}`: Get incoming citations to a paper (supports DOI, PMID, OMID) + - `/citation-count/{id}`: Get citation count (supports DOI, PMID, OMID) + - `/reference-count/{id}`: Get reference count (supports DOI, PMID, OMID) +- **Rate Limiting**: 180 requests/minute per IP +- **Authentication**: Optional access token via `authorization` header +- **Response Format**: JSON (default) or CSV + +### Response Structure +```json +[ + { + "oci": "06180334099-062603917082", + "citing": "omid:br/06180334099 doi:10.1108/jd-12-2013-0166 openalex:W1977728350", + "cited": "omid:br/062603917082 doi:10.1038/35079151 doi:10.1038/nature28042 pmid:11484021", + "creation": "2015-03-09", + "timespan": "P13Y9M9D", + "journal_sc": "no", + "author_sc": "no" + } +] +``` + +--- + +## Implementation Approach + +### Architecture Pattern +Follow the existing citation fetcher pattern used by SemanticScholarCitationFetcher: + +1. **Main Fetcher Class**: `OpenCitationsFetcher` implements `CitationFetcher` interface +2. **Response Model Classes**: Create POJOs for JSON deserialization +3. **DOI-based Fetching**: Primary identifier will be DOI (most common in JabRef) +4. **URL Construction**: Separate URL building for better logging (follows ADR-0014) + +### Class Structure + +#### Main Class +**Location**: `jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java` + +**Responsibilities**: +- Implement `CitationFetcher` interface methods: + - `getCitations(BibEntry entry)`: Returns papers citing the given entry + - `getReferences(BibEntry entry)`: Returns papers referenced by the given entry + - `getCitationCount(BibEntry entry)`: Returns citation count + - `getName()`: Returns "OpenCitations" +- Construct API URLs with proper DOI formatting +- Handle HTTP requests using `URLDownload` +- Parse JSON responses using Gson +- Convert OpenCitations citation data to BibEntry objects +- Handle errors (missing DOI, API failures, empty responses) + +#### Response Model Classes +**Location**: `jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/` + +1. **`CitationResponse.java`**: Array wrapper for citation/reference responses +2. **`CitationItem.java`**: Individual citation record with fields: + - `oci`: Open Citation Identifier + - `citing`: PIDs of citing entity (space-separated: "omid:xxx doi:xxx pmid:xxx") + - `cited`: PIDs of cited entity + - `creation`: Publication date (ISO format: "YYYY-MM-DD") + - `timespan`: Duration between publications (XSD format: "P1Y2M3D") + - `journal_sc`: Journal self-citation flag ("yes"/"no") + - `author_sc`: Author self-citation flag ("yes"/"no") +3. **`CountResponse.java`**: Single count value response + - `count`: Integer count value + +### DOI Extraction Logic + +OpenCitations returns PIDs in format: `"omid:br/06180334099 doi:10.1108/jd-12-2013-0166 openalex:W1977728350"` + +Strategy: +1. Split PID string by spaces +2. Look for entry starting with "doi:" +3. Extract DOI value after "doi:" prefix +4. Use DOI to fetch full BibEntry via existing DOI fetcher (DoiFetcher or CrossRef) +5. Fallback: If no DOI, check for PMID and use PMID fetcher + +### Key Implementation Details + +1. **DOI Requirement**: Only process entries that have a DOI field + ```java + if (entry.getDOI().isEmpty()) { + return List.of(); + } + ``` + +2. **URL Construction**: + ```java + private String getApiUrl(String endpoint, BibEntry entry) { + String doi = entry.getDOI().orElseThrow().asString(); + return API_BASE_URL + "/" + endpoint + "/doi:" + doi; + } + ``` + +3. **BibEntry Conversion**: + - Extract DOI from `citing` or `cited` field based on request type + - Use DoiFetcher to get complete BibEntry + - If DOI fetching fails, create minimal BibEntry with DOI field only + +4. **Error Handling**: + - Catch `MalformedURLException` → throw `FetcherException("Malformed URL", e)` + - Catch `IOException` → throw `FetcherException("Could not read from OpenCitations", e)` + - Handle empty responses → return empty list + - Handle missing DOI in PID string → skip entry or use fallback + +5. **Optional API Key Support**: + ```java + importerPreferences.getApiKey(getName()) + .ifPresent(apiKey -> urlDownload.addHeader("authorization", apiKey)); + ``` + +--- + +## Source Code Structure Changes + +### New Files to Create + +#### Main Implementation +``` +jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/ +├── OpenCitationsFetcher.java (Main fetcher implementation) +├── CitationResponse.java (Response wrapper - array of CitationItem) +├── CitationItem.java (Individual citation record) +└── CountResponse.java (Count response wrapper) +``` + +#### Test Files +``` +jablib/src/test/java/org/jabref/logic/importer/fetcher/citation/opencitations/ +└── OpenCitationsFetcherTest.java (Unit tests) +``` + +### Files to Modify + +1. **`jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java`** + - Add `OPENCITATIONS("OpenCitations")` enum constant + - Add case in `getCitationFetcher()` switch statement + +--- + +## Data Model Changes + +### CitationItem Structure + +```java +public class CitationItem { + private String oci; // Open Citation Identifier + private String citing; // Space-separated PIDs of citing work + private String cited; // Space-separated PIDs of cited work + private String creation; // ISO date: "2015-03-09" + private String timespan; // XSD duration: "P13Y9M9D" + + @SerializedName("journal_sc") + private String journalSelfCitation; // "yes" or "no" + + @SerializedName("author_sc") + private String authorSelfCitation; // "yes" or "no" + + // Getters, setters, and helper methods + public String extractDoi(); // Extract DOI from citing/cited string + public BibEntry toBibEntry(); // Convert to BibEntry using DoiFetcher +} +``` + +### No Interface Changes +The `CitationFetcher` interface remains unchanged. OpenCitationsFetcher implements all existing methods. + +--- + +## Verification Approach + +### Unit Tests +**File**: `OpenCitationsFetcherTest.java` + +**Test Cases**: +1. **`testGetReferencesWithValidDoi()`** + - Use a well-known DOI (e.g., "10.1108/jd-12-2013-0166") + - Verify list is not empty + - Verify returned entries have DOI fields + - Verify at least one entry has title/author + +2. **`testGetCitationsWithValidDoi()`** + - Use a well-cited DOI + - Verify list is not empty + - Check structure of returned entries + +3. **`testGetCitationCount()`** + - Verify count is returned as Optional + - Verify count is greater than 0 for well-cited paper + +4. **`testEmptyWhenNoDoi()`** + - Pass entry without DOI + - Verify empty list is returned + +5. **`testGetName()`** + - Verify getName() returns "OpenCitations" + +6. **`testMalformedResponse()`** (if needed) + - Test handling of unexpected API responses + +### Test Annotation +Use `@FetcherTest` annotation (existing in codebase) to mark tests that require network access. + +### Manual Testing +1. Test with various DOIs from different sources +2. Verify rate limiting behavior (180 req/min) +3. Test with/without API key +4. Verify integration with JabRef GUI (if applicable) + +### Build Commands +```bash +# Run tests +./gradlew :jablib:test --tests "*OpenCitations*" + +# Run all fetcher tests +./gradlew :jablib:test --tests "*.citation.*" + +# Check code style (if applicable) +./gradlew :jablib:checkstyleMain +``` + +--- + +## Implementation Considerations + +### Edge Cases +1. **Missing DOI in entry**: Return empty list immediately +2. **DOI in response but no metadata**: Create minimal BibEntry with just DOI +3. **Multiple DOIs in PID string**: Take first one +4. **PMID-only citations**: Future enhancement (not in initial version) +5. **Empty API response**: Return empty list (not an error) +6. **Rate limiting**: API handles this; no special client-side handling needed + +### Performance +- Each API call fetches all results (no pagination needed, API returns complete list) +- DOI resolution for each citation may be slow (network calls) +- Consider caching DOI lookups (future enhancement) + +### Security +- No API key stored in code (uses ImporterPreferences) +- HTTPS only (built into API URL) + +### Logging +- Log API URLs before requests (DEBUG level) +- Log response parsing errors (WARN level) +- Follow existing pattern from SemanticScholarCitationFetcher + +--- + +## Dependencies & Compatibility + +### Required Dependencies +All dependencies already present in JabRef: +- `com.google.code.gson:gson` (JSON parsing) +- JabRef's fetcher infrastructure +- JUnit 5 + Mockito (testing) + +### Compatibility +- Works with JabRef's existing DOI fetcher integration +- Compatible with ImporterPreferences system +- Supports optional API key configuration +- No breaking changes to existing code + +--- + +## Future Enhancements (Out of Scope) + +1. Support PMID and OMID identifiers (currently DOI-only) +2. Client-side rate limiting to prevent API quota exhaustion +3. Caching of DOI resolutions +4. Support for CSV response format +5. Batch fetching for multiple entries +6. Metadata enrichment using OpenCitations data (timespan, self-citation flags) + +--- + +## Summary + +This is a **medium-complexity** task requiring: +- Creation of 4 new Java classes (1 fetcher + 3 models) +- Modification of 1 existing enum +- Unit tests following existing patterns +- No complex algorithms or data structures +- Straightforward REST API integration +- Good documentation via OpenCitations API docs + +The implementation closely follows existing patterns (especially SemanticScholarCitationFetcher), minimizing architectural decisions and risks. 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..cde135ede0a --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationItem.java @@ -0,0 +1,97 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import java.util.Optional; + +import com.google.gson.annotations.SerializedName; + +public class CitationItem { + private String oci; + private String citing; + private String cited; + private String creation; + private String timespan; + + @SerializedName("journal_sc") + private String journalSelfCitation; + + @SerializedName("author_sc") + private String authorSelfCitation; + + public String getOci() { + return oci; + } + + public void setOci(String oci) { + this.oci = oci; + } + + public String getCiting() { + return citing; + } + + public void setCiting(String citing) { + this.citing = citing; + } + + public String getCited() { + return cited; + } + + public void setCited(String cited) { + this.cited = cited; + } + + public String getCreation() { + return creation; + } + + public void setCreation(String creation) { + this.creation = creation; + } + + public String getTimespan() { + return timespan; + } + + public void setTimespan(String timespan) { + this.timespan = timespan; + } + + public String getJournalSelfCitation() { + return journalSelfCitation; + } + + public void setJournalSelfCitation(String journalSelfCitation) { + this.journalSelfCitation = journalSelfCitation; + } + + public String getAuthorSelfCitation() { + return authorSelfCitation; + } + + public void setAuthorSelfCitation(String authorSelfCitation) { + this.authorSelfCitation = authorSelfCitation; + } + + public Optional extractDoi(String pidString) { + if (pidString == null || pidString.isEmpty()) { + return Optional.empty(); + } + + String[] pids = pidString.split("\\s+"); + for (String pid : pids) { + if (pid.startsWith("doi:")) { + return Optional.of(pid.substring(4)); + } + } + return Optional.empty(); + } + + public Optional getCitingDoi() { + return extractDoi(citing); + } + + public Optional getCitedDoi() { + return extractDoi(cited); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java new file mode 100644 index 00000000000..3d469e8b8be --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java @@ -0,0 +1,23 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +import java.util.List; + +public class CitationResponse { + private List citations; + + public CitationResponse() { + this.citations = List.of(); + } + + public CitationResponse(List citations) { + this.citations = citations; + } + + public List getCitations() { + return citations; + } + + public void setCitations(List citations) { + this.citations = citations; + } +} 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..08c8fc2b70d --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CountResponse.java @@ -0,0 +1,21 @@ +package org.jabref.logic.importer.fetcher.citation.opencitations; + +public class CountResponse { + private String count; + + public String getCount() { + return count; + } + + public void setCount(String count) { + this.count = count; + } + + public int getCountAsInt() { + try { + return Integer.parseInt(count); + } catch (NumberFormatException e) { + return 0; + } + } +} From 0c627404e9d8bfff927ac002a9551e5340b6158d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:38:05 +0100 Subject: [PATCH 05/31] Technical Specification Updated spec.md and plan.md to use **package-private fields** instead of private fields with getters/setters: **Changes:** - Response model classes will use fields without access modifiers (package-private) - No getters/setters boilerplate needed - Gson can directly access package-private fields - Only public helper methods (extractDoi(), toBibEntry()) where needed --- .zenflow/tasks/new-task-ed21/plan.md | 6 +++--- .zenflow/tasks/new-task-ed21/spec.md | 28 ++++++++++++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md index b7355e50e94..eeae7485767 100644 --- a/.zenflow/tasks/new-task-ed21/plan.md +++ b/.zenflow/tasks/new-task-ed21/plan.md @@ -60,9 +60,9 @@ Create the data model classes for OpenCitations API responses: - `CountResponse.java`: Wrapper for count responses Each class should: -- Include Gson annotations where needed (e.g., @SerializedName) -- Provide getters/setters -- Add helper method in CitationItem to extract DOI from PID string +- Use package-private fields (no access modifier) - no getters/setters needed +- Include Gson annotations where needed (e.g., @SerializedName for "journal_sc", "author_sc") +- Add public helper methods in CitationItem: extractDoi() and toBibEntry() **Verification**: Models should compile without errors and follow JabRef code style. diff --git a/.zenflow/tasks/new-task-ed21/spec.md b/.zenflow/tasks/new-task-ed21/spec.md index 0d92d054bcb..893247f029d 100644 --- a/.zenflow/tasks/new-task-ed21/spec.md +++ b/.zenflow/tasks/new-task-ed21/spec.md @@ -84,16 +84,18 @@ Follow the existing citation fetcher pattern used by SemanticScholarCitationFetc **Location**: `jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/` 1. **`CitationResponse.java`**: Array wrapper for citation/reference responses -2. **`CitationItem.java`**: Individual citation record with fields: +2. **`CitationItem.java`**: Individual citation record with fields (package-private, no getters/setters): - `oci`: Open Citation Identifier - `citing`: PIDs of citing entity (space-separated: "omid:xxx doi:xxx pmid:xxx") - `cited`: PIDs of cited entity - `creation`: Publication date (ISO format: "YYYY-MM-DD") - `timespan`: Duration between publications (XSD format: "P1Y2M3D") - - `journal_sc`: Journal self-citation flag ("yes"/"no") - - `author_sc`: Author self-citation flag ("yes"/"no") + - `journal_sc`: Journal self-citation flag ("yes"/"no") - use @SerializedName("journal_sc") + - `author_sc`: Author self-citation flag ("yes"/"no") - use @SerializedName("author_sc") 3. **`CountResponse.java`**: Single count value response - - `count`: Integer count value + - `count`: Integer count value (package-private) + +**Note**: Use package-private fields (no access modifier) instead of private fields to avoid getters/setters boilerplate. ### DOI Extraction Logic @@ -175,24 +177,26 @@ jablib/src/test/java/org/jabref/logic/importer/fetcher/citation/opencitations/ ```java public class CitationItem { - private String oci; // Open Citation Identifier - private String citing; // Space-separated PIDs of citing work - private String cited; // Space-separated PIDs of cited work - private String creation; // ISO date: "2015-03-09" - private String timespan; // XSD duration: "P13Y9M9D" + String oci; // Open Citation Identifier (package-private) + String citing; // Space-separated PIDs of citing work + String cited; // Space-separated PIDs of cited work + String creation; // ISO date: "2015-03-09" + String timespan; // XSD duration: "P13Y9M9D" @SerializedName("journal_sc") - private String journalSelfCitation; // "yes" or "no" + String journalSelfCitation; // "yes" or "no" @SerializedName("author_sc") - private String authorSelfCitation; // "yes" or "no" + String authorSelfCitation; // "yes" or "no" - // Getters, setters, and helper methods + // Helper methods (public) public String extractDoi(); // Extract DOI from citing/cited string public BibEntry toBibEntry(); // Convert to BibEntry using DoiFetcher } ``` +**Design Choice**: Use package-private fields (no access modifier) to reduce boilerplate. Gson can access package-private fields for JSON deserialization. + ### No Interface Changes The `CitationFetcher` interface remains unchanged. OpenCitationsFetcher implements all existing methods. From 312562ba164cbb2821a9bebbdd13789f9e54a909 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 13:40:22 +0100 Subject: [PATCH 06/31] Technical Specification Updated spec.md with complete structure examples for all response model classes: **CitationItem**: Package-private fields (oci, citing, cited, creation, timespan, journalSelfCitation, authorSelfCitation) with public helper methods **CitationResponse**: Package-private `List data` field **CountResponse**: Package-private `String count` field All classes follow the package-private pattern - no getters/setters needed, Gson handles deserialization directly. --- .zenflow/tasks/new-task-ed21/spec.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.zenflow/tasks/new-task-ed21/spec.md b/.zenflow/tasks/new-task-ed21/spec.md index 893247f029d..8e48a69301d 100644 --- a/.zenflow/tasks/new-task-ed21/spec.md +++ b/.zenflow/tasks/new-task-ed21/spec.md @@ -197,6 +197,24 @@ public class CitationItem { **Design Choice**: Use package-private fields (no access modifier) to reduce boilerplate. Gson can access package-private fields for JSON deserialization. +### CitationResponse Structure + +```java +/// Used for GSON +public class CitationResponse { + List data; // Package-private, no getter needed +} +``` + +### CountResponse Structure + +```java +/// Used for GSON +public class CountResponse { + String count; // Package-private, returned as string from API +} +``` + ### No Interface Changes The `CitationFetcher` interface remains unchanged. OpenCitationsFetcher implements all existing methods. From d6545b1cf0bbb2ddb77f2e793fe4acd48b32c391 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:23:27 +0100 Subject: [PATCH 07/31] Create Response Model Classes --- .../citation/opencitations/CitationItem.java | 79 +++---------------- .../opencitations/CitationResponse.java | 23 ------ .../citation/opencitations/CountResponse.java | 17 ++-- 3 files changed, 19 insertions(+), 100 deletions(-) delete mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java 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 index cde135ede0a..ca5d83c8639 100644 --- 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 @@ -3,77 +3,22 @@ import java.util.Optional; import com.google.gson.annotations.SerializedName; +import org.jspecify.annotations.Nullable; -public class CitationItem { - private String oci; - private String citing; - private String cited; - private String creation; - private String timespan; +class CitationItem { + @Nullable String oci; + @Nullable String citing; + @Nullable String cited; + @Nullable String creation; + @Nullable String timespan; @SerializedName("journal_sc") - private String journalSelfCitation; + @Nullable String journalSelfCitation; @SerializedName("author_sc") - private String authorSelfCitation; + @Nullable String authorSelfCitation; - public String getOci() { - return oci; - } - - public void setOci(String oci) { - this.oci = oci; - } - - public String getCiting() { - return citing; - } - - public void setCiting(String citing) { - this.citing = citing; - } - - public String getCited() { - return cited; - } - - public void setCited(String cited) { - this.cited = cited; - } - - public String getCreation() { - return creation; - } - - public void setCreation(String creation) { - this.creation = creation; - } - - public String getTimespan() { - return timespan; - } - - public void setTimespan(String timespan) { - this.timespan = timespan; - } - - public String getJournalSelfCitation() { - return journalSelfCitation; - } - - public void setJournalSelfCitation(String journalSelfCitation) { - this.journalSelfCitation = journalSelfCitation; - } - - public String getAuthorSelfCitation() { - return authorSelfCitation; - } - - public void setAuthorSelfCitation(String authorSelfCitation) { - this.authorSelfCitation = authorSelfCitation; - } - - public Optional extractDoi(String pidString) { + Optional extractDoi(@Nullable String pidString) { if (pidString == null || pidString.isEmpty()) { return Optional.empty(); } @@ -87,11 +32,11 @@ public Optional extractDoi(String pidString) { return Optional.empty(); } - public Optional getCitingDoi() { + Optional citingDoi() { return extractDoi(citing); } - public Optional getCitedDoi() { + Optional citedDoi() { return extractDoi(cited); } } diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java deleted file mode 100644 index 3d469e8b8be..00000000000 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/CitationResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.jabref.logic.importer.fetcher.citation.opencitations; - -import java.util.List; - -public class CitationResponse { - private List citations; - - public CitationResponse() { - this.citations = List.of(); - } - - public CitationResponse(List citations) { - this.citations = citations; - } - - public List getCitations() { - return citations; - } - - public void setCitations(List citations) { - this.citations = citations; - } -} 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 index 08c8fc2b70d..aeb9255ea92 100644 --- 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 @@ -1,17 +1,14 @@ package org.jabref.logic.importer.fetcher.citation.opencitations; -public class CountResponse { - private String count; +import org.jspecify.annotations.Nullable; - public String getCount() { - return count; - } - - public void setCount(String count) { - this.count = count; - } +class CountResponse { + @Nullable String count; - public int getCountAsInt() { + int countAsInt() { + if (count == null) { + return 0; + } try { return Integer.parseInt(count); } catch (NumberFormatException e) { From 6004e2746c600f77abede781de828d98260fcc3d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:25:24 +0100 Subject: [PATCH 08/31] Create Response Model Classes --- .../importer/fetcher/citation/opencitations/CitationItem.java | 2 ++ .../importer/fetcher/citation/opencitations/CountResponse.java | 2 ++ 2 files changed, 4 insertions(+) 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 index ca5d83c8639..af686d09d6b 100644 --- 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 @@ -3,8 +3,10 @@ import java.util.Optional; 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; 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 index aeb9255ea92..ef18ee0145b 100644 --- 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 @@ -1,7 +1,9 @@ package org.jabref.logic.importer.fetcher.citation.opencitations; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +@NullMarked class CountResponse { @Nullable String count; From d3e6beb4b27406b8f117b43b6436cc2ee3ec64b5 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:28:37 +0100 Subject: [PATCH 09/31] Implement OpenCitationsFetcher Core Class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✓ **OpenCitationsFetcher Core Class implemented** Created `/home/koppor/.zenflow/worktrees/new-task-ed21/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java` with: - ✓ Constructor accepting `ImporterPreferences` - ✓ `getName()` returning "OpenCitations" - ✓ `getReferences()` - fetches outgoing references from DOI - ✓ `getCitations()` - fetches incoming citations to DOI - ✓ `getCitationCount()` - fetches citation count - ✓ DOI extraction from OpenCitations PID strings - ✓ BibEntry conversion using CrossRef fetcher - ✓ Fallback to minimal BibEntry creation - ✓ Error handling (missing DOI, network errors, JSON parsing) - ✓ Optional API key support via ImporterPreferences Plan updated: `plan.md:71` marked as `[x]` --- .zenflow/tasks/new-task-ed21/plan.md | 3 +- .../opencitations/OpenCitationsFetcher.java | 180 ++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md index eeae7485767..7665e7fcbc4 100644 --- a/.zenflow/tasks/new-task-ed21/plan.md +++ b/.zenflow/tasks/new-task-ed21/plan.md @@ -68,7 +68,8 @@ Each class should: --- -### [ ] Step: Implement OpenCitationsFetcher Core Class +### [x] Step: Implement OpenCitationsFetcher Core Class + Implement `OpenCitationsFetcher` class that implements `CitationFetcher` interface: - Constructor accepting `ImporterPreferences` 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..2a3a99184fc --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java @@ -0,0 +1,180 @@ +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 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 com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 @NonNull List<@NonNull BibEntry> getReferences(@NonNull BibEntry entry) throws FetcherException { + if (entry.getDOI().isEmpty()) { + return List.of(); + } + + String apiUrl = getApiUrl("references", entry); + LOGGER.debug("References 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(); + CitationItem[] citationItems = GSON.fromJson(jsonResponse, CitationItem[].class); + + if (citationItems == null || citationItems.length == 0) { + return List.of(); + } + + List entries = new ArrayList<>(); + for (CitationItem item : citationItems) { + Optional doi = item.citedDoi(); + doi.ifPresent(doiString -> { + try { + BibEntry bibEntry = fetchBibEntryFromDoi(doiString); + entries.add(bibEntry); + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiString, e); + entries.add(createMinimalBibEntry(doiString)); + } + }); + } + + 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); + } + } + + @Override + public @NonNull List<@NonNull BibEntry> getCitations(@NonNull BibEntry entry) throws FetcherException { + if (entry.getDOI().isEmpty()) { + return List.of(); + } + + String apiUrl = getApiUrl("citations", entry); + LOGGER.debug("Citations 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(); + CitationItem[] citationItems = GSON.fromJson(jsonResponse, CitationItem[].class); + + if (citationItems == null || citationItems.length == 0) { + return List.of(); + } + + List entries = new ArrayList<>(); + for (CitationItem item : citationItems) { + Optional doi = item.citingDoi(); + doi.ifPresent(doiString -> { + try { + BibEntry bibEntry = fetchBibEntryFromDoi(doiString); + entries.add(bibEntry); + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiString, e); + entries.add(createMinimalBibEntry(doiString)); + } + }); + } + + 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); + } + } + + @Override + public Optional getCitationCount(@NonNull 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); + } + } + + private BibEntry fetchBibEntryFromDoi(String doi) throws FetcherException { + return crossRefFetcher.performSearchById(doi) + .orElseThrow(() -> new FetcherException("Could not fetch BibEntry for DOI: " + doi)); + } + + private BibEntry createMinimalBibEntry(String doi) { + BibEntry bibEntry = new BibEntry(); + bibEntry.setField(StandardField.DOI, doi); + bibEntry.setChanged(true); + return bibEntry; + } +} From 345bac076d452ba2b4b12776d7b36132fab29975 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:31:02 +0100 Subject: [PATCH 10/31] Implement OpenCitationsFetcher Core Class Updated to use `@NullMarked` + `@Nullable` pattern --- .../citation/opencitations/OpenCitationsFetcher.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index 2a3a99184fc..93932a507f3 100644 --- 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 @@ -17,10 +17,11 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; -import org.jspecify.annotations.NonNull; +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"; @@ -48,7 +49,7 @@ private String getApiUrl(String endpoint, BibEntry entry) throws FetcherExceptio } @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(); } @@ -92,7 +93,7 @@ private String getApiUrl(String endpoint, BibEntry entry) throws FetcherExceptio } @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(); } @@ -136,7 +137,7 @@ private String getApiUrl(String endpoint, BibEntry entry) throws FetcherExceptio } @Override - public Optional getCitationCount(@NonNull BibEntry entry) throws FetcherException { + public Optional getCitationCount(BibEntry entry) throws FetcherException { if (entry.getDOI().isEmpty()) { return Optional.empty(); } From 4e64795939bceb430032b1c662eb0f3bffc81a63 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:36:34 +0100 Subject: [PATCH 11/31] Implement OpenCitationsFetcher Core Class Done. `fetchBibEntryFromDoi` now handles errors with `createMinimalBibEntry`, and `createMinimalBibEntry` uses method chaining. --- .../opencitations/OpenCitationsFetcher.java | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) 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 index 93932a507f3..a91e8875a4e 100644 --- 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 @@ -73,15 +73,7 @@ public List getReferences(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { Optional doi = item.citedDoi(); - doi.ifPresent(doiString -> { - try { - BibEntry bibEntry = fetchBibEntryFromDoi(doiString); - entries.add(bibEntry); - } catch (FetcherException e) { - LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiString, e); - entries.add(createMinimalBibEntry(doiString)); - } - }); + doi.ifPresent(doiString -> entries.add(fetchBibEntryFromDoi(doiString))); } return entries; @@ -117,15 +109,7 @@ public List getCitations(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { Optional doi = item.citingDoi(); - doi.ifPresent(doiString -> { - try { - BibEntry bibEntry = fetchBibEntryFromDoi(doiString); - entries.add(bibEntry); - } catch (FetcherException e) { - LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiString, e); - entries.add(createMinimalBibEntry(doiString)); - } - }); + doi.ifPresent(doiString -> entries.add(fetchBibEntryFromDoi(doiString))); } return entries; @@ -167,15 +151,19 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio } } - private BibEntry fetchBibEntryFromDoi(String doi) throws FetcherException { - return crossRefFetcher.performSearchById(doi) - .orElseThrow(() -> new FetcherException("Could not fetch BibEntry for DOI: " + doi)); + private BibEntry fetchBibEntryFromDoi(String doi) { + try { + return crossRefFetcher.performSearchById(doi) + .orElseGet(() -> createMinimalBibEntry(doi)); + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", doi, e); + return createMinimalBibEntry(doi); + } } private BibEntry createMinimalBibEntry(String doi) { - BibEntry bibEntry = new BibEntry(); - bibEntry.setField(StandardField.DOI, doi); - bibEntry.setChanged(true); - return bibEntry; + return new BibEntry() + .withField(StandardField.DOI, doi) + .withChanged(true); } } From 5406e4f0e781109102b1a422d372c05dea9d8bcb Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:47:18 +0100 Subject: [PATCH 12/31] Implement OpenCitationsFetcher Core Class Done. Now handles all identifiers: - **DOI**: Fetches full metadata via CrossRef - **PMID**: Creates entry with `StandardField.PMID` - **Other IDs** (omid, openalex): Creates entry using `FieldFactory.parseField()` No identifiers are skipped. --- .../citation/opencitations/CitationItem.java | 49 ++++++++++++++++--- .../opencitations/OpenCitationsFetcher.java | 29 ++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) 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 index af686d09d6b..5f959a0d312 100644 --- 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 @@ -2,6 +2,10 @@ import java.util.Optional; +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; @@ -20,25 +24,56 @@ class CitationItem { @SerializedName("author_sc") @Nullable String authorSelfCitation; - Optional extractDoi(@Nullable String pidString) { + record IdentifierWithField(Field field, String value) {} + + Optional extractIdentifier(@Nullable String pidString, String prefix) { + if (pidString == null || pidString.isEmpty()) { + return Optional.empty(); + } + + String[] pids = pidString.split("\\s+"); + for (String pid : pids) { + if (pid.startsWith(prefix + ":")) { + return Optional.of(pid.substring(prefix.length() + 1)); + } + } + return Optional.empty(); + } + + Optional extractBestIdentifier(@Nullable String pidString) { if (pidString == null || pidString.isEmpty()) { return Optional.empty(); } + Optional doi = extractIdentifier(pidString, "doi"); + if (doi.isPresent()) { + return Optional.of(new IdentifierWithField(StandardField.DOI, doi.get())); + } + + Optional pmid = extractIdentifier(pidString, "pmid"); + if (pmid.isPresent()) { + return Optional.of(new IdentifierWithField(StandardField.PMID, pmid.get())); + } + String[] pids = pidString.split("\\s+"); for (String pid : pids) { - if (pid.startsWith("doi:")) { - return Optional.of(pid.substring(4)); + int colonIndex = pid.indexOf(':'); + if (colonIndex > 0) { + String prefix = pid.substring(0, colonIndex); + String value = pid.substring(colonIndex + 1); + Field field = FieldFactory.parseField(prefix); + return Optional.of(new IdentifierWithField(field, value)); } } + return Optional.empty(); } - Optional citingDoi() { - return extractDoi(citing); + Optional citingIdentifier() { + return extractBestIdentifier(citing); } - Optional citedDoi() { - return extractDoi(cited); + Optional citedIdentifier() { + return extractBestIdentifier(cited); } } 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 index a91e8875a4e..e5a8e16e112 100644 --- 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 @@ -72,8 +72,8 @@ public List getReferences(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { - Optional doi = item.citedDoi(); - doi.ifPresent(doiString -> entries.add(fetchBibEntryFromDoi(doiString))); + item.citedIdentifier().ifPresent(identifier -> + entries.add(fetchBibEntryFromIdentifier(identifier))); } return entries; @@ -108,8 +108,8 @@ public List getCitations(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { - Optional doi = item.citingDoi(); - doi.ifPresent(doiString -> entries.add(fetchBibEntryFromDoi(doiString))); + item.citingIdentifier().ifPresent(identifier -> + entries.add(fetchBibEntryFromIdentifier(identifier))); } return entries; @@ -151,19 +151,22 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio } } - private BibEntry fetchBibEntryFromDoi(String doi) { - try { - return crossRefFetcher.performSearchById(doi) - .orElseGet(() -> createMinimalBibEntry(doi)); - } catch (FetcherException e) { - LOGGER.warn("Could not fetch BibEntry for DOI: {}", doi, e); - return createMinimalBibEntry(doi); + private BibEntry fetchBibEntryFromIdentifier(CitationItem.IdentifierWithField identifier) { + if (identifier.field().equals(StandardField.DOI)) { + try { + return crossRefFetcher.performSearchById(identifier.value()) + .orElseGet(() -> createMinimalBibEntry(identifier)); + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", identifier.value(), e); + return createMinimalBibEntry(identifier); + } } + return createMinimalBibEntry(identifier); } - private BibEntry createMinimalBibEntry(String doi) { + private BibEntry createMinimalBibEntry(CitationItem.IdentifierWithField identifier) { return new BibEntry() - .withField(StandardField.DOI, doi) + .withField(identifier.field(), identifier.value()) .withChanged(true); } } From b13ffec7c0a82b558660ff640febd1bb7246114a Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:51:57 +0100 Subject: [PATCH 13/31] Implement OpenCitationsFetcher Core Class Simplified. `FieldFactory.parseField()` handles all identifiers including `doi` and `pmid`. --- .../citation/opencitations/CitationItem.java | 25 ------------------- 1 file changed, 25 deletions(-) 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 index 5f959a0d312..bc92c3877ac 100644 --- 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 @@ -4,7 +4,6 @@ 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; @@ -26,35 +25,11 @@ class CitationItem { record IdentifierWithField(Field field, String value) {} - Optional extractIdentifier(@Nullable String pidString, String prefix) { - if (pidString == null || pidString.isEmpty()) { - return Optional.empty(); - } - - String[] pids = pidString.split("\\s+"); - for (String pid : pids) { - if (pid.startsWith(prefix + ":")) { - return Optional.of(pid.substring(prefix.length() + 1)); - } - } - return Optional.empty(); - } - Optional extractBestIdentifier(@Nullable String pidString) { if (pidString == null || pidString.isEmpty()) { return Optional.empty(); } - Optional doi = extractIdentifier(pidString, "doi"); - if (doi.isPresent()) { - return Optional.of(new IdentifierWithField(StandardField.DOI, doi.get())); - } - - Optional pmid = extractIdentifier(pidString, "pmid"); - if (pmid.isPresent()) { - return Optional.of(new IdentifierWithField(StandardField.PMID, pmid.get())); - } - String[] pids = pidString.split("\\s+"); for (String pid : pids) { int colonIndex = pid.indexOf(':'); From f3867e1cc602b1e2b9d5861611b9e53993bbd6f7 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:54:18 +0100 Subject: [PATCH 14/31] Implement OpenCitationsFetcher Core Class Updated to handle all identifiers from space-separated lists. Each citation/reference now creates a single BibEntry with: - Full metadata from CrossRef (if DOI present) - All additional identifiers (PMID, OMID, OpenAlex, etc.) added as fields --- .../citation/opencitations/CitationItem.java | 20 ++++---- .../opencitations/OpenCitationsFetcher.java | 51 ++++++++++++------- 2 files changed, 45 insertions(+), 26 deletions(-) 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 index bc92c3877ac..cfa93133e79 100644 --- 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 @@ -1,6 +1,7 @@ package org.jabref.logic.importer.fetcher.citation.opencitations; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -25,30 +26,31 @@ class CitationItem { record IdentifierWithField(Field field, String value) {} - Optional extractBestIdentifier(@Nullable String pidString) { + List extractIdentifiers(@Nullable String pidString) { if (pidString == null || pidString.isEmpty()) { - return Optional.empty(); + 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); - return Optional.of(new IdentifierWithField(field, value)); + identifiers.add(new IdentifierWithField(field, value)); } } - return Optional.empty(); + return identifiers; } - Optional citingIdentifier() { - return extractBestIdentifier(citing); + List citingIdentifiers() { + return extractIdentifiers(citing); } - Optional citedIdentifier() { - return extractBestIdentifier(cited); + List citedIdentifiers() { + return extractIdentifiers(cited); } } 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 index e5a8e16e112..20f5c66410e 100644 --- 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 @@ -72,8 +72,10 @@ public List getReferences(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { - item.citedIdentifier().ifPresent(identifier -> - entries.add(fetchBibEntryFromIdentifier(identifier))); + List identifiers = item.citedIdentifiers(); + if (!identifiers.isEmpty()) { + entries.add(fetchBibEntryFromIdentifiers(identifiers)); + } } return entries; @@ -108,8 +110,10 @@ public List getCitations(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { - item.citingIdentifier().ifPresent(identifier -> - entries.add(fetchBibEntryFromIdentifier(identifier))); + List identifiers = item.citingIdentifiers(); + if (!identifiers.isEmpty()) { + entries.add(fetchBibEntryFromIdentifiers(identifiers)); + } } return entries; @@ -151,22 +155,35 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio } } - private BibEntry fetchBibEntryFromIdentifier(CitationItem.IdentifierWithField identifier) { - if (identifier.field().equals(StandardField.DOI)) { - try { - return crossRefFetcher.performSearchById(identifier.value()) - .orElseGet(() -> createMinimalBibEntry(identifier)); - } catch (FetcherException e) { - LOGGER.warn("Could not fetch BibEntry for DOI: {}", identifier.value(), e); - return createMinimalBibEntry(identifier); + private BibEntry fetchBibEntryFromIdentifiers(List identifiers) { + for (CitationItem.IdentifierWithField identifier : identifiers) { + if (identifier.field().equals(StandardField.DOI)) { + try { + Optional bibEntry = crossRefFetcher.performSearchById(identifier.value()); + if (bibEntry.isPresent()) { + BibEntry entry = bibEntry.get(); + for (CitationItem.IdentifierWithField id : identifiers) { + if (!entry.hasField(id.field())) { + entry.setField(id.field(), id.value()); + } + } + return entry; + } + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", identifier.value(), e); + } + break; } } - return createMinimalBibEntry(identifier); + return createBibEntryFromIdentifiers(identifiers); } - private BibEntry createMinimalBibEntry(CitationItem.IdentifierWithField identifier) { - return new BibEntry() - .withField(identifier.field(), identifier.value()) - .withChanged(true); + private BibEntry createBibEntryFromIdentifiers(List identifiers) { + BibEntry bibEntry = new BibEntry(); + for (CitationItem.IdentifierWithField identifier : identifiers) { + bibEntry.setField(identifier.field(), identifier.value()); + } + bibEntry.setChanged(true); + return bibEntry; } } From 057abaffbe91298bf962f6001fef96f755e3e7f9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 16:55:54 +0100 Subject: [PATCH 15/31] Implement OpenCitationsFetcher Core Class Done. Identifiers without colons are now added to BibEntry with `StandardField.NOTE`. --- .../importer/fetcher/citation/opencitations/CitationItem.java | 3 +++ 1 file changed, 3 insertions(+) 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 index cfa93133e79..dd1a47c5591 100644 --- 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 @@ -5,6 +5,7 @@ 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; @@ -40,6 +41,8 @@ List extractIdentifiers(@Nullable String pidString) { 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)); } } From 5f77b88ffc0f27342982f752f6aed4ee59c8238b Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 20:51:47 +0100 Subject: [PATCH 16/31] Implement OpenCitationsFetcher Core Class Unified. `getReferences()` and `getCitations()` now delegate to `fetchCitationData()` with endpoint and identifier extractor as parameters. --- .../opencitations/OpenCitationsFetcher.java | 46 ++++--------------- 1 file changed, 9 insertions(+), 37 deletions(-) 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 index 20f5c66410e..38aac38f6df 100644 --- 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 @@ -5,6 +5,7 @@ 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; @@ -50,50 +51,21 @@ private String getApiUrl(String endpoint, BibEntry entry) throws FetcherExceptio @Override public List getReferences(BibEntry entry) throws FetcherException { - if (entry.getDOI().isEmpty()) { - return List.of(); - } - - String apiUrl = getApiUrl("references", entry); - LOGGER.debug("References 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(); - 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 = item.citedIdentifiers(); - 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); - } + return fetchCitationData(entry, "references", CitationItem::citedIdentifiers); } @Override public List getCitations(BibEntry entry) throws FetcherException { + return fetchCitationData(entry, "citations", CitationItem::citingIdentifiers); + } + + private List fetchCitationData(BibEntry entry, String endpoint, Function> identifierExtractor) throws FetcherException { if (entry.getDOI().isEmpty()) { return List.of(); } - String apiUrl = getApiUrl("citations", entry); - LOGGER.debug("Citations URL: {}", apiUrl); + String apiUrl = getApiUrl(endpoint, entry); + LOGGER.debug("{} URL: {}", endpoint, apiUrl); try { URL url = URLUtil.create(apiUrl); @@ -110,7 +82,7 @@ public List getCitations(BibEntry entry) throws FetcherException { List entries = new ArrayList<>(); for (CitationItem item : citationItems) { - List identifiers = item.citingIdentifiers(); + List identifiers = identifierExtractor.apply(item); if (!identifiers.isEmpty()) { entries.add(fetchBibEntryFromIdentifiers(identifiers)); } From 84fd50f3d86c6cff3589e0f187fab7dbc7b679a7 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 20:54:05 +0100 Subject: [PATCH 17/31] Implement OpenCitationsFetcher Core Class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored. Now creates **one BibEntry per identifier**: - Each identifier (DOI, PMID, OMID, etc.) → separate BibEntry - DOI identifiers fetch full metadata via CrossRef - Other identifiers create minimal entries with just that field --- .../opencitations/OpenCitationsFetcher.java | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) 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 index 38aac38f6df..4fd53f885fc 100644 --- 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 @@ -83,9 +83,7 @@ private List fetchCitationData(BibEntry entry, String endpoint, Functi List entries = new ArrayList<>(); for (CitationItem item : citationItems) { List identifiers = identifierExtractor.apply(item); - if (!identifiers.isEmpty()) { - entries.add(fetchBibEntryFromIdentifiers(identifiers)); - } + entries.addAll(fetchBibEntriesFromIdentifiers(identifiers)); } return entries; @@ -127,35 +125,30 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio } } - private BibEntry fetchBibEntryFromIdentifiers(List identifiers) { + private List fetchBibEntriesFromIdentifiers(List identifiers) { + List entries = new ArrayList<>(); for (CitationItem.IdentifierWithField identifier : identifiers) { - if (identifier.field().equals(StandardField.DOI)) { - try { - Optional bibEntry = crossRefFetcher.performSearchById(identifier.value()); - if (bibEntry.isPresent()) { - BibEntry entry = bibEntry.get(); - for (CitationItem.IdentifierWithField id : identifiers) { - if (!entry.hasField(id.field())) { - entry.setField(id.field(), id.value()); - } - } - return entry; - } - } catch (FetcherException e) { - LOGGER.warn("Could not fetch BibEntry for DOI: {}", identifier.value(), e); - } - break; - } + entries.add(fetchBibEntryFromIdentifier(identifier)); } - return createBibEntryFromIdentifiers(identifiers); + return entries; } - private BibEntry createBibEntryFromIdentifiers(List identifiers) { - BibEntry bibEntry = new BibEntry(); - for (CitationItem.IdentifierWithField identifier : identifiers) { - bibEntry.setField(identifier.field(), identifier.value()); + private BibEntry fetchBibEntryFromIdentifier(CitationItem.IdentifierWithField identifier) { + if (identifier.field().equals(StandardField.DOI)) { + try { + return crossRefFetcher.performSearchById(identifier.value()) + .orElseGet(() -> createMinimalBibEntry(identifier)); + } catch (FetcherException e) { + LOGGER.warn("Could not fetch BibEntry for DOI: {}", identifier.value(), e); + return createMinimalBibEntry(identifier); + } } - bibEntry.setChanged(true); - return bibEntry; + return createMinimalBibEntry(identifier); + } + + private BibEntry createMinimalBibEntry(CitationItem.IdentifierWithField identifier) { + return new BibEntry() + .withField(identifier.field(), identifier.value()) + .withChanged(true); } } From 3538b3d9fbd562603d3caab6f5880018fe2b2df7 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 20:57:31 +0100 Subject: [PATCH 18/31] Implement OpenCitationsFetcher Core Class Fixed! Each `CitationItem` now creates **one BibEntry** with: - Full metadata from CrossRef (if DOI exists) + all other identifiers added - All identifiers as fields (if no DOI) --- .../opencitations/OpenCitationsFetcher.java | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) 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 index 4fd53f885fc..893a906966b 100644 --- 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 @@ -83,7 +83,9 @@ private List fetchCitationData(BibEntry entry, String endpoint, Functi List entries = new ArrayList<>(); for (CitationItem item : citationItems) { List identifiers = identifierExtractor.apply(item); - entries.addAll(fetchBibEntriesFromIdentifiers(identifiers)); + if (!identifiers.isEmpty()) { + entries.add(fetchBibEntryFromIdentifiers(identifiers)); + } } return entries; @@ -125,30 +127,33 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio } } - private List fetchBibEntriesFromIdentifiers(List identifiers) { - List entries = new ArrayList<>(); - for (CitationItem.IdentifierWithField identifier : identifiers) { - entries.add(fetchBibEntryFromIdentifier(identifier)); - } - return entries; - } + private BibEntry fetchBibEntryFromIdentifiers(List identifiers) { + Optional doiIdentifier = identifiers.stream() + .filter(id -> id.field().equals(StandardField.DOI)) + .findFirst(); - private BibEntry fetchBibEntryFromIdentifier(CitationItem.IdentifierWithField identifier) { - if (identifier.field().equals(StandardField.DOI)) { + if (doiIdentifier.isPresent()) { try { - return crossRefFetcher.performSearchById(identifier.value()) - .orElseGet(() -> createMinimalBibEntry(identifier)); + 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: {}", identifier.value(), e); - return createMinimalBibEntry(identifier); + LOGGER.warn("Could not fetch BibEntry for DOI: {}", doiIdentifier.get().value(), e); } } - return createMinimalBibEntry(identifier); - } - private BibEntry createMinimalBibEntry(CitationItem.IdentifierWithField identifier) { - return new BibEntry() - .withField(identifier.field(), identifier.value()) - .withChanged(true); + BibEntry bibEntry = new BibEntry(); + for (CitationItem.IdentifierWithField identifier : identifiers) { + bibEntry.setField(identifier.field(), identifier.value()); + } + bibEntry.setChanged(true); + return bibEntry; } } From 8a488ec2f28f1c6ed5f3f0f3e67620f3a4e093ba Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 21:01:30 +0100 Subject: [PATCH 19/31] Register OpenCitations in CitationFetcherType Enum I'll register OpenCitations in the CitationFetcherType enum. Let me first locate and read the file. --- .zenflow/tasks/new-task-ed21/plan.md | 1 + .../fetcher/citation/opencitations/OpenCitationsFetcher.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md index 7665e7fcbc4..ea787929e0f 100644 --- a/.zenflow/tasks/new-task-ed21/plan.md +++ b/.zenflow/tasks/new-task-ed21/plan.md @@ -87,6 +87,7 @@ Implement `OpenCitationsFetcher` class that implements `CitationFetcher` interfa --- ### [ ] Step: Register OpenCitations in CitationFetcherType Enum + Update `CitationFetcherType.java`: - Add `OPENCITATIONS("OpenCitations")` enum constant 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 index 893a906966b..f40ebbade8d 100644 --- 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 @@ -153,7 +153,6 @@ private BibEntry fetchBibEntryFromIdentifiers(List Date: Sun, 1 Feb 2026 21:04:28 +0100 Subject: [PATCH 20/31] Remove zenflow things --- .zenflow/tasks/new-task-ed21/plan.md | 124 ---------- .zenflow/tasks/new-task-ed21/spec.md | 340 --------------------------- 2 files changed, 464 deletions(-) delete mode 100644 .zenflow/tasks/new-task-ed21/plan.md delete mode 100644 .zenflow/tasks/new-task-ed21/spec.md diff --git a/.zenflow/tasks/new-task-ed21/plan.md b/.zenflow/tasks/new-task-ed21/plan.md deleted file mode 100644 index ea787929e0f..00000000000 --- a/.zenflow/tasks/new-task-ed21/plan.md +++ /dev/null @@ -1,124 +0,0 @@ -# Spec and build - -## Configuration -- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}` - ---- - -## Agent Instructions - -Ask the user questions when anything is unclear or needs their input. This includes: -- Ambiguous or incomplete requirements -- Technical decisions that affect architecture or user experience -- Trade-offs that require business context - -Do not make assumptions on important decisions — get clarification first. - ---- - -## Workflow Steps - -### [x] Step: Technical Specification - - -Assess the task's difficulty, as underestimating it leads to poor outcomes. -- easy: Straightforward implementation, trivial bug fix or feature -- medium: Moderate complexity, some edge cases or caveats to consider -- hard: Complex logic, many caveats, architectural considerations, or high-risk changes - -Create a technical specification for the task that is appropriate for the complexity level: -- Review the existing codebase architecture and identify reusable components. -- Define the implementation approach based on established patterns in the project. -- Identify all source code files that will be created or modified. -- Define any necessary data model, API, or interface changes. -- Describe verification steps using the project's test and lint commands. - -Save the output to `{@artifacts_path}/spec.md` with: -- Technical context (language, dependencies) -- Implementation approach -- Source code structure changes -- Data model / API / interface changes -- Verification approach - -If the task is complex enough, create a detailed implementation plan based on `{@artifacts_path}/spec.md`: -- Break down the work into concrete tasks (incrementable, testable milestones) -- Each task should reference relevant contracts and include verification steps -- Replace the Implementation step below with the planned tasks - -Rule of thumb for step size: each step should represent a coherent unit of work (e.g., implement a component, add an API endpoint, write tests for a module). Avoid steps that are too granular (single function). - -Save to `{@artifacts_path}/plan.md`. If the feature is trivial and doesn't warrant this breakdown, keep the Implementation step below as is. - ---- - -### [x] Step: Create Response Model Classes - - -Create the data model classes for OpenCitations API responses: -- `CitationItem.java`: Individual citation record with OCI, citing/cited PIDs, creation date, timespan -- `CitationResponse.java`: Array wrapper for citations/references -- `CountResponse.java`: Wrapper for count responses - -Each class should: -- Use package-private fields (no access modifier) - no getters/setters needed -- Include Gson annotations where needed (e.g., @SerializedName for "journal_sc", "author_sc") -- Add public helper methods in CitationItem: extractDoi() and toBibEntry() - -**Verification**: Models should compile without errors and follow JabRef code style. - ---- - -### [x] Step: Implement OpenCitationsFetcher Core Class - - -Implement `OpenCitationsFetcher` class that implements `CitationFetcher` interface: -- Constructor accepting `ImporterPreferences` -- `getName()` returning "OpenCitations" -- URL construction methods for references, citations, and counts -- Implement `getReferences()`: fetch outgoing references -- Implement `getCitations()`: fetch incoming citations -- Implement `getCitationCount()`: fetch citation count -- DOI extraction and BibEntry conversion logic -- Error handling for missing DOI, network errors, malformed responses -- Optional API key support via ImporterPreferences - -**Verification**: Class compiles, implements all required interface methods. - ---- - -### [ ] Step: Register OpenCitations in CitationFetcherType Enum - - -Update `CitationFetcherType.java`: -- Add `OPENCITATIONS("OpenCitations")` enum constant -- Add case in `getCitationFetcher()` switch to instantiate `OpenCitationsFetcher` - -**Verification**: Enum compiles, factory method returns correct fetcher instance. - ---- - -### [ ] Step: Implement Unit Tests - -Create `OpenCitationsFetcherTest.java` with test cases: -- `testGetReferencesWithValidDoi()`: Test reference fetching with known DOI -- `testGetCitationsWithValidDoi()`: Test citation fetching with known DOI -- `testGetCitationCount()`: Verify citation count retrieval -- `testEmptyWhenNoDoi()`: Verify empty list when DOI missing -- `testGetName()`: Verify fetcher name - -Mark test class with `@FetcherTest` annotation. - -**Verification**: Run `./gradlew :jablib:test --tests "*OpenCitations*"` - all tests pass. - ---- - -### [ ] Step: Final Verification and Reporting - -1. Run full test suite for citation fetchers -2. Verify code style compliance (if checkstyle configured) -3. Manual smoke test with JabRef (if applicable) -4. Write completion report to `{@artifacts_path}/report.md` with: - - Summary of what was implemented - - Test results - - Any challenges or issues encountered - - Known limitations (e.g., DOI-only support) diff --git a/.zenflow/tasks/new-task-ed21/spec.md b/.zenflow/tasks/new-task-ed21/spec.md deleted file mode 100644 index 8e48a69301d..00000000000 --- a/.zenflow/tasks/new-task-ed21/spec.md +++ /dev/null @@ -1,340 +0,0 @@ -# Technical Specification: OpenCitations Citation Fetcher - -## Task Difficulty Assessment - -**Difficulty Level: Medium** - -**Rationale:** -- The implementation follows established patterns in the codebase (SemanticScholarCitationFetcher, CrossRefCitationFetcher) -- OpenCitations API is well-documented with a straightforward REST API structure -- Response parsing is simple JSON (no complex nested structures) -- No authentication required (API key is optional for rate limit increase) -- Some edge cases need handling (DOI extraction from PIDs, empty responses) -- Requires creating response model classes and proper error handling - ---- - -## Technical Context - -### Language & Dependencies -- **Language**: Java 21+ -- **Key Dependencies**: - - Gson (JSON parsing) - already used in SemanticScholarCitationFetcher - - Jackson (alternative, used in CrossRefCitationFetcher) - - JUnit 5 (testing) - - Mockito (mocking in tests) - - JabRef's existing fetcher infrastructure - -### API Details -- **Base URL**: `https://api.opencitations.net/index/v2` -- **Key Endpoints**: - - `/references/{id}`: Get outgoing references from a paper (supports DOI, PMID, OMID) - - `/citations/{id}`: Get incoming citations to a paper (supports DOI, PMID, OMID) - - `/citation-count/{id}`: Get citation count (supports DOI, PMID, OMID) - - `/reference-count/{id}`: Get reference count (supports DOI, PMID, OMID) -- **Rate Limiting**: 180 requests/minute per IP -- **Authentication**: Optional access token via `authorization` header -- **Response Format**: JSON (default) or CSV - -### Response Structure -```json -[ - { - "oci": "06180334099-062603917082", - "citing": "omid:br/06180334099 doi:10.1108/jd-12-2013-0166 openalex:W1977728350", - "cited": "omid:br/062603917082 doi:10.1038/35079151 doi:10.1038/nature28042 pmid:11484021", - "creation": "2015-03-09", - "timespan": "P13Y9M9D", - "journal_sc": "no", - "author_sc": "no" - } -] -``` - ---- - -## Implementation Approach - -### Architecture Pattern -Follow the existing citation fetcher pattern used by SemanticScholarCitationFetcher: - -1. **Main Fetcher Class**: `OpenCitationsFetcher` implements `CitationFetcher` interface -2. **Response Model Classes**: Create POJOs for JSON deserialization -3. **DOI-based Fetching**: Primary identifier will be DOI (most common in JabRef) -4. **URL Construction**: Separate URL building for better logging (follows ADR-0014) - -### Class Structure - -#### Main Class -**Location**: `jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/OpenCitationsFetcher.java` - -**Responsibilities**: -- Implement `CitationFetcher` interface methods: - - `getCitations(BibEntry entry)`: Returns papers citing the given entry - - `getReferences(BibEntry entry)`: Returns papers referenced by the given entry - - `getCitationCount(BibEntry entry)`: Returns citation count - - `getName()`: Returns "OpenCitations" -- Construct API URLs with proper DOI formatting -- Handle HTTP requests using `URLDownload` -- Parse JSON responses using Gson -- Convert OpenCitations citation data to BibEntry objects -- Handle errors (missing DOI, API failures, empty responses) - -#### Response Model Classes -**Location**: `jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/` - -1. **`CitationResponse.java`**: Array wrapper for citation/reference responses -2. **`CitationItem.java`**: Individual citation record with fields (package-private, no getters/setters): - - `oci`: Open Citation Identifier - - `citing`: PIDs of citing entity (space-separated: "omid:xxx doi:xxx pmid:xxx") - - `cited`: PIDs of cited entity - - `creation`: Publication date (ISO format: "YYYY-MM-DD") - - `timespan`: Duration between publications (XSD format: "P1Y2M3D") - - `journal_sc`: Journal self-citation flag ("yes"/"no") - use @SerializedName("journal_sc") - - `author_sc`: Author self-citation flag ("yes"/"no") - use @SerializedName("author_sc") -3. **`CountResponse.java`**: Single count value response - - `count`: Integer count value (package-private) - -**Note**: Use package-private fields (no access modifier) instead of private fields to avoid getters/setters boilerplate. - -### DOI Extraction Logic - -OpenCitations returns PIDs in format: `"omid:br/06180334099 doi:10.1108/jd-12-2013-0166 openalex:W1977728350"` - -Strategy: -1. Split PID string by spaces -2. Look for entry starting with "doi:" -3. Extract DOI value after "doi:" prefix -4. Use DOI to fetch full BibEntry via existing DOI fetcher (DoiFetcher or CrossRef) -5. Fallback: If no DOI, check for PMID and use PMID fetcher - -### Key Implementation Details - -1. **DOI Requirement**: Only process entries that have a DOI field - ```java - if (entry.getDOI().isEmpty()) { - return List.of(); - } - ``` - -2. **URL Construction**: - ```java - private String getApiUrl(String endpoint, BibEntry entry) { - String doi = entry.getDOI().orElseThrow().asString(); - return API_BASE_URL + "/" + endpoint + "/doi:" + doi; - } - ``` - -3. **BibEntry Conversion**: - - Extract DOI from `citing` or `cited` field based on request type - - Use DoiFetcher to get complete BibEntry - - If DOI fetching fails, create minimal BibEntry with DOI field only - -4. **Error Handling**: - - Catch `MalformedURLException` → throw `FetcherException("Malformed URL", e)` - - Catch `IOException` → throw `FetcherException("Could not read from OpenCitations", e)` - - Handle empty responses → return empty list - - Handle missing DOI in PID string → skip entry or use fallback - -5. **Optional API Key Support**: - ```java - importerPreferences.getApiKey(getName()) - .ifPresent(apiKey -> urlDownload.addHeader("authorization", apiKey)); - ``` - ---- - -## Source Code Structure Changes - -### New Files to Create - -#### Main Implementation -``` -jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/opencitations/ -├── OpenCitationsFetcher.java (Main fetcher implementation) -├── CitationResponse.java (Response wrapper - array of CitationItem) -├── CitationItem.java (Individual citation record) -└── CountResponse.java (Count response wrapper) -``` - -#### Test Files -``` -jablib/src/test/java/org/jabref/logic/importer/fetcher/citation/opencitations/ -└── OpenCitationsFetcherTest.java (Unit tests) -``` - -### Files to Modify - -1. **`jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcherType.java`** - - Add `OPENCITATIONS("OpenCitations")` enum constant - - Add case in `getCitationFetcher()` switch statement - ---- - -## Data Model Changes - -### CitationItem Structure - -```java -public class CitationItem { - String oci; // Open Citation Identifier (package-private) - String citing; // Space-separated PIDs of citing work - String cited; // Space-separated PIDs of cited work - String creation; // ISO date: "2015-03-09" - String timespan; // XSD duration: "P13Y9M9D" - - @SerializedName("journal_sc") - String journalSelfCitation; // "yes" or "no" - - @SerializedName("author_sc") - String authorSelfCitation; // "yes" or "no" - - // Helper methods (public) - public String extractDoi(); // Extract DOI from citing/cited string - public BibEntry toBibEntry(); // Convert to BibEntry using DoiFetcher -} -``` - -**Design Choice**: Use package-private fields (no access modifier) to reduce boilerplate. Gson can access package-private fields for JSON deserialization. - -### CitationResponse Structure - -```java -/// Used for GSON -public class CitationResponse { - List data; // Package-private, no getter needed -} -``` - -### CountResponse Structure - -```java -/// Used for GSON -public class CountResponse { - String count; // Package-private, returned as string from API -} -``` - -### No Interface Changes -The `CitationFetcher` interface remains unchanged. OpenCitationsFetcher implements all existing methods. - ---- - -## Verification Approach - -### Unit Tests -**File**: `OpenCitationsFetcherTest.java` - -**Test Cases**: -1. **`testGetReferencesWithValidDoi()`** - - Use a well-known DOI (e.g., "10.1108/jd-12-2013-0166") - - Verify list is not empty - - Verify returned entries have DOI fields - - Verify at least one entry has title/author - -2. **`testGetCitationsWithValidDoi()`** - - Use a well-cited DOI - - Verify list is not empty - - Check structure of returned entries - -3. **`testGetCitationCount()`** - - Verify count is returned as Optional - - Verify count is greater than 0 for well-cited paper - -4. **`testEmptyWhenNoDoi()`** - - Pass entry without DOI - - Verify empty list is returned - -5. **`testGetName()`** - - Verify getName() returns "OpenCitations" - -6. **`testMalformedResponse()`** (if needed) - - Test handling of unexpected API responses - -### Test Annotation -Use `@FetcherTest` annotation (existing in codebase) to mark tests that require network access. - -### Manual Testing -1. Test with various DOIs from different sources -2. Verify rate limiting behavior (180 req/min) -3. Test with/without API key -4. Verify integration with JabRef GUI (if applicable) - -### Build Commands -```bash -# Run tests -./gradlew :jablib:test --tests "*OpenCitations*" - -# Run all fetcher tests -./gradlew :jablib:test --tests "*.citation.*" - -# Check code style (if applicable) -./gradlew :jablib:checkstyleMain -``` - ---- - -## Implementation Considerations - -### Edge Cases -1. **Missing DOI in entry**: Return empty list immediately -2. **DOI in response but no metadata**: Create minimal BibEntry with just DOI -3. **Multiple DOIs in PID string**: Take first one -4. **PMID-only citations**: Future enhancement (not in initial version) -5. **Empty API response**: Return empty list (not an error) -6. **Rate limiting**: API handles this; no special client-side handling needed - -### Performance -- Each API call fetches all results (no pagination needed, API returns complete list) -- DOI resolution for each citation may be slow (network calls) -- Consider caching DOI lookups (future enhancement) - -### Security -- No API key stored in code (uses ImporterPreferences) -- HTTPS only (built into API URL) - -### Logging -- Log API URLs before requests (DEBUG level) -- Log response parsing errors (WARN level) -- Follow existing pattern from SemanticScholarCitationFetcher - ---- - -## Dependencies & Compatibility - -### Required Dependencies -All dependencies already present in JabRef: -- `com.google.code.gson:gson` (JSON parsing) -- JabRef's fetcher infrastructure -- JUnit 5 + Mockito (testing) - -### Compatibility -- Works with JabRef's existing DOI fetcher integration -- Compatible with ImporterPreferences system -- Supports optional API key configuration -- No breaking changes to existing code - ---- - -## Future Enhancements (Out of Scope) - -1. Support PMID and OMID identifiers (currently DOI-only) -2. Client-side rate limiting to prevent API quota exhaustion -3. Caching of DOI resolutions -4. Support for CSV response format -5. Batch fetching for multiple entries -6. Metadata enrichment using OpenCitations data (timespan, self-citation flags) - ---- - -## Summary - -This is a **medium-complexity** task requiring: -- Creation of 4 new Java classes (1 fetcher + 3 models) -- Modification of 1 existing enum -- Unit tests following existing patterns -- No complex algorithms or data structures -- Straightforward REST API integration -- Good documentation via OpenCitations API docs - -The implementation closely follows existing patterns (especially SemanticScholarCitationFetcher), minimizing architectural decisions and risks. From 3979fdc7208bd437eb30a435646033ef5043dc7d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 21:36:47 +0100 Subject: [PATCH 21/31] JabKit: Use OpenCitations instead of CrossRef --- .../toolkit/commands/GetCitedWorks.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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; From 2f811eee655066144fe2dab5f2e144aed2c14647 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 21:37:08 +0100 Subject: [PATCH 22/31] Add selection of citation providers also for GetCitingWorks --- .../toolkit/commands/GetCitingWorks.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) 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; From 345465475a6de2a509637bc7556459f8aa67c720 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 21:37:26 +0100 Subject: [PATCH 23/31] Begin to wire OPEN_CITATIONS into GUI --- .../importer/fetcher/citation/CitationFetcherType.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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); }; From 343b6f5afc42b231cc4af5dab53797792030fdf4 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 21:37:40 +0100 Subject: [PATCH 24/31] Refine code --- .../citation/opencitations/OpenCitationsFetcher.java | 8 +++++--- .../SemanticScholarCitationFetcher.java | 11 ++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) 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 index f40ebbade8d..63c3a1a26d5 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -60,7 +61,8 @@ public List getCitations(BibEntry entry) throws FetcherException { } private List fetchCitationData(BibEntry entry, String endpoint, Function> identifierExtractor) throws FetcherException { - if (entry.getDOI().isEmpty()) { + Optional doi = entry.getDOI(); + if (doi.isEmpty()) { return List.of(); } @@ -129,8 +131,8 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio private BibEntry fetchBibEntryFromIdentifiers(List identifiers) { Optional doiIdentifier = identifiers.stream() - .filter(id -> id.field().equals(StandardField.DOI)) - .findFirst(); + .filter(id -> id.field().equals(StandardField.DOI)) + .findFirst(); if (doiIdentifier.isPresent()) { try { 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(); } From 15a15370b712aec1b8b41302c7a750f0501dfb39 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 22:16:00 +0100 Subject: [PATCH 25/31] Wire OpenCitations into GUI --- .../CitationRelationsTab.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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; From 6bff9a9fda024d4667a432a11718d9badf0e37c3 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 22:17:33 +0100 Subject: [PATCH 26/31] Add CHANGELOG.md entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c43b77b1a..7bd0e621b7f 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`). - 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) From 8869a0b55e7c5abb949b9425a4462206b269e931 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 23:01:12 +0100 Subject: [PATCH 27/31] Fix CrossRef fetching issue --- CHANGELOG.md | 1 + .../java/org/jabref/logic/importer/fetcher/CrossRef.java | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd0e621b7f..2358cd04226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,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`). - 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/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(); } From dd3dabc6db62c07959cc082ba4acfe434e8cd644 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 23:02:00 +0100 Subject: [PATCH 28/31] Add link to CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2358cd04226..725dc8f528f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +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`). +- 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) @@ -45,7 +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`). +- 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) From 64e5d270e3fcfaa8312a2fec79610cb2f6a02298 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 23:06:17 +0100 Subject: [PATCH 29/31] Add links to API - and group methods more together --- .../opencitations/OpenCitationsFetcher.java | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) 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 index 63c3a1a26d5..7c8332db8c8 100644 --- 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 @@ -60,6 +60,7 @@ 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()) { @@ -98,6 +99,36 @@ private List fetchCitationData(BibEntry entry, String endpoint, Functi } } + 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()) { @@ -128,33 +159,4 @@ public Optional getCitationCount(BibEntry entry) throws FetcherExceptio 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; - } } From 75e377edee62ea217398c42eb4fcef1d1d19e3d4 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 23:57:41 +0100 Subject: [PATCH 30/31] Fix references.md --- docs/glossary/references.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary/references.md b/docs/glossary/references.md index 7c1469e841a..5ff49234696 100644 --- a/docs/glossary/references.md +++ b/docs/glossary/references.md @@ -10,7 +10,7 @@ parent: Glossary **References** is a structured list of works cited in a scientific work. -It is usually described as the outgoing references to other cited works appearing in the reference list or the +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. From dcd6c313601dbcd6f9e294918ca1308bcd88c43a Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 1 Feb 2026 23:58:33 +0100 Subject: [PATCH 31/31] Fix format --- .../importer/fetcher/citation/opencitations/CitationItem.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index dd1a47c5591..8bafd547f91 100644 --- 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 @@ -25,7 +25,8 @@ class CitationItem { @SerializedName("author_sc") @Nullable String authorSelfCitation; - record IdentifierWithField(Field field, String value) {} + record IdentifierWithField(Field field, String value) { + } List extractIdentifiers(@Nullable String pidString) { if (pidString == null || pidString.isEmpty()) {