Skip to content

Add OpenAlex-based Citation Fetcher#15023

Merged
koppor merged 34 commits intomainfrom
add-open-alex
Feb 4, 2026
Merged

Add OpenAlex-based Citation Fetcher#15023
koppor merged 34 commits intomainfrom
add-open-alex

Conversation

@koppor
Copy link
Copy Markdown
Member

@koppor koppor commented Feb 4, 2026

User description

Adds https://docs.openalex.org/ to Citations Tab.

Small refactorings, too.

image image

Steps to test

  1. Open a library
  2. Click on tab "Citations"
  3. Select "OpenAlex"

Even better: Get free API key at https://openalex.org/settings/api - and configure JabRef to use it.

Mandatory checks


PR Type

Enhancement


Description

  • Add OpenAlex as citation fetcher with API key support

  • Refactor repository architecture to use single MVStore file

  • Rename repository methods for consistency (insert→add, read→get)

  • Improve error handling and null safety in fetchers

  • Add SemanticScholar to customizable key fetchers list


Diagram Walkthrough

flowchart LR
  OA["OpenAlex Fetcher"] -->|implements| CF["CitationFetcher"]
  OA -->|implements| CKF["CustomizableKeyFetcher"]
  OA -->|API Key| IP["ImporterPreferences"]
  CR["Citation Repository"] -->|uses single| MVS["MVStore File"]
  CR -->|refactored methods| RM["add/get instead of insert/read"]
  SS["SemanticScholar"] -->|added to| CKF
Loading

File Walkthrough

Relevant files
Enhancement
10 files
OpenAlex.java
Implement CitationFetcher with API key support                     
+232/-71
BibEntryCitationsAndReferencesRepositoryShell.java
Consolidate to single MVStore file architecture                   
+49/-23 
BibEntryCitationsAndReferencesRepository.java
Rename methods for API consistency                                             
+4/-4     
MVStoreBibEntryRelationRepository.java
Refactor to accept MVStore instance                                           
+5/-28   
BibEntryRelationRepository.java
Remove close method from interface                                             
+0/-2     
SearchCitationsRelationsService.java
Update to use refactored repository methods                           
+5/-5     
WebFetchers.java
Add OpenAlex and SemanticScholar to customizable fetchers
+15/-11 
CitationFetcherType.java
Add OpenAlex citation fetcher type enum                                   
+4/-0     
FulltextFetchers.java
Improve variable naming and initialization                             
+6/-4     
BibEntry.java
Allow null values in setField for cleaner code                     
+5/-5     
Documentation
4 files
CustomizableKeyFetcher.java
Add documentation about registration requirement                 
+2/-0     
DOI.java
Add documentation comments and clarifications                       
+6/-3     
CHANGELOG.md
Document OpenAlex and SemanticScholar additions                   
+6/-2     
fetchers.md
Add OpenAlex to fetcher documentation table                           
+12/-11 
Configuration changes
5 files
BuildInfo.java
Add OpenAlex API key configuration                                             
+2/-0     
tests-code-fetchers.yml
Add OpenAlex API key environment variable                               
+1/-0     
tests-code.yml
Add OpenAlex API key environment variable                               
+2/-1     
build.gradle.kts
Configure OpenAlex API key build property                               
+3/-0     
build.properties
Add OpenAlex API key property and reorder                               
+5/-3     
Bug fix
1 files
CitationRelationsTab.java
Use getLocalizedMessage for better error display                 
+5/-4     
Tests
7 files
BibEntryCitationsAndReferencesRepositoryShellTest.java
Update tests for refactored repository methods                     
+11/-13 
MVStoreBibEntryRelationRepositoryTest.java
Adapt tests to MVStore instance injection                               
+32/-55 
BibEntryRelationsRepositoryTestHelpers.java
Update test helpers for renamed methods                                   
+8/-8     
OpenAlexFetcherTest.java
Add ImporterPreferences mock to tests                                       
+13/-1   
OpenAlexCitationFetcherTest.java
Add new citation fetcher tests                                                     
+57/-0   
HTMLCharacterCheckerTest.java
Remove null handling test                                                               
+0/-9     
BibEntryTest.java
Remove null citation key test                                                       
+0/-6     
Formatting
2 files
MedlineFetcherTest.java
Minor formatting adjustment                                                           
+1/-0     
JabRefGUI.java
Minor whitespace formatting                                                           
+1/-0     
Additional files
2 files
WebFetcher.java +3/-0     
SemanticScholar.java +0/-3     

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Feb 4, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Null pointer dereference

Description: The mvStore.close() method is called on a potentially null field without null checking,
which could cause a NullPointerException when the testing constructor is used (where
mvStore is set to null).
BibEntryCitationsAndReferencesRepositoryShell.java [123-123]

Referred Code
    this.mvStore.close();
}
URL parameter injection

Description: API key from user preferences is directly added to URL parameters without validation or
sanitization, potentially allowing injection of malicious query parameters if the API key
contains special characters like & or =.
OpenAlex.java [71-71]

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
uriBuilder.addParameter("search", new DefaultQueryTransformer().transformSearchQuery(queryNode).orElse(""));
URL parameter injection

Description: API key from user preferences is directly added to URL parameters without validation or
sanitization in the getUrl method, potentially allowing injection of malicious query
parameters.
OpenAlex.java [111-111]

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
String fieldList = fieldsToSelect.stream()
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent Exception Handling: The code catches RuntimeException and logs at debug level without providing actionable
error context or ensuring proper error propagation in all cases.

Referred Code
    } catch (FetcherClientException e) {
        String redactedUrl = FetcherException.getRedactedUrl(url.toString());
        if (e.getHttpResponse().isPresent()) {
            int code = e.getHttpResponse().get().statusCode();
            if (code == 404) {
                LOGGER.trace("Work not found at URL: {}", redactedUrl);
            } else {
                LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
            }
        } else {
            LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
        }
        return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
    } catch (RuntimeException e) {
        String redactedUrl = FetcherException.getRedactedUrl(url.toString());
        LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
        return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
    }
}));

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
API Key Logging: The code logs URLs that may contain API keys as query parameters, potentially exposing
sensitive credentials in log files.

Referred Code
LOGGER.debug("URL for query: {}", result);
return result;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Feb 4, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Correctly parse work ID from URL
Suggestion Impact:The commit addressed the underlying issue identified by the suggestion but implemented a more comprehensive solution. Instead of simply extracting the ID from the URL in workUrlsToBibEntryStream, the commit created a dedicated extractOpenAlexId() method (lines 21-42) that properly parses OpenAlex URLs and extracts IDs. This method is then used in the refactored getUrl(BibEntry entry, List fieldsToSelect) method (lines 50-99) to handle URL-based lookups. The workUrlsToBibEntryStream method at line 145 still passes workUrl directly to getUrl, but the URL parsing logic is now properly handled within the getUrl chain through extractOpenAlexId().

code diff:

+    @VisibleForTesting
+    Optional<String> extractOpenAlexId(String url) {
+        try {
+            URL u = new URL(url);
+
+            if (!"openalex.org".equalsIgnoreCase(u.getHost())) {
+                return Optional.empty();
+            }
+
+            String path = u.getPath();          // "/works/W4408614692" or "/W4408614692"
+            if (path.isBlank() || "/".equals(path)) {
+                return Optional.empty();
+            }
+
+            String[] segments = path.split("/");
+            String id = segments[segments.length - 1];
+
+            return id.isBlank() ? Optional.empty() : Optional.of(id);
+        } catch (MalformedURLException e) {
+            return Optional.empty();
+        }
+    }
+
     @Override
     public URL getURLForQuery(BaseQueryNode queryNode) throws URISyntaxException, MalformedURLException {
         URIBuilder uriBuilder = new URIBuilder(URL_PATTERN);
@@ -76,36 +99,27 @@
     }
 
     private Optional<URL> getUrl(BibEntry entry, List<String> fieldsToSelect) throws MalformedURLException {
-        Optional<DOI> doiOpt = entry.getField(StandardField.DOI)
-                                    .flatMap(DOI::findInText);
-        if (doiOpt.isPresent()) {
-            return Optional.of(getUrl("/" + doiOpt.get().getURIAsASCIIString(), fieldsToSelect));
-        }
-        Optional<String> urlOpt = entry.getField(StandardField.URL);
-        if (urlOpt.isPresent()) {
-            String url = urlOpt.get();
-            String lower = url.toLowerCase();
-            try {
-                int idx = lower.indexOf("openalex.org/");
-                if (idx >= 0) {
-                    String tail = url.substring(idx + "openalex.org/".length());
-                    LOGGER.debug("URL for query: {}", tail);
-                    int queryIdx = tail.indexOf('?');
-                    if (queryIdx >= 0) {
-                        tail = tail.substring(0, queryIdx);
-                    }
-                    if (!tail.isBlank()) {
-                        return Optional.of(getUrl("/" + tail, fieldsToSelect));
-                    }
-                }
-            } catch (MalformedURLException ignored) {
-                throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
-            }
-        }
-        return Optional.empty();
-    }
-
-    private URL getUrl(String tail, List<String> fieldsToSelect) throws MalformedURLException {
+        try {
+            // Supported identifiers for lookup are listed at https://docs.openalex.org/api-entities/works/work-object#ids
+            return entry.getField(StandardField.DOI)
+                        .flatMap(DOI::findInText)
+                        .map(Unchecked.function(doi -> getUrl("/" + doi.getURIAsASCIIString(), fieldsToSelect)))
+
+                        .or(() -> entry.getField(StandardField.PMID)
+                                       // URL: See https://docs.openalex.org/api-entities/works/get-a-single-work#external-ids
+                                       .map(Unchecked.function(pmid -> getUrl("/pmid:" + pmid, fieldsToSelect))))
+
+                        // Fallback: OpenAlex identifier from URL
+                        .or(() -> entry.getField(StandardField.URL)
+                                       .flatMap(this::extractOpenAlexId)
+                                       .map(Unchecked.function(id -> getUrl("/" + id, fieldsToSelect))));
+        } catch (RuntimeException ignored) {
+            LOGGER.debug("Invalid OpenAlex URL");
+            return Optional.empty();
+        }
+    }

In workUrlsToBibEntryStream, extract the work ID from the full workUrl by taking
the substring after the last / before passing it to getUrl to ensure correct API
request URL construction.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [289-315]

     private Stream<BibEntry> workUrlsToBibEntryStream(JSONArray workUrlArray) {
         return IntStream.range(0, workUrlArray.length())
                         .mapToObj(workUrlArray::getString)
-                        .map(Unchecked.function(workUrl -> getUrl("/" + workUrl, List.of())))
+                        .map(workUrl -> workUrl.substring(workUrl.lastIndexOf('/') + 1))
+                        .map(Unchecked.function(workId -> getUrl("/" + workId, List.of())))
                         .map(Unchecked.function(url -> {
                             try (ProgressInputStream stream = getUrlDownload(url).asInputStream()) {
                                 return jsonItemToBibEntry(JsonReader.toJsonObject(stream));
                             } catch (FetcherClientException e) {
 ...
                                 return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
                             } catch (RuntimeException e) {
                                 String redactedUrl = FetcherException.getRedactedUrl(url.toString());
                                 LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
                                 return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
                             }
                         }));
     }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical bug in URL construction that would cause citation and reference fetching to fail. Extracting the work ID from the full URL is necessary for the getUrl method to function correctly.

High
Correctly parse publication date from API
Suggestion Impact:The commit directly implements the suggestion by changing line 131 from String.valueOf(item.optInt("publication_date")) to item.optString("publication_date"). Additionally, the commit also applies the same fix to publication_year on line 134, changing it from optInt to optString, which follows the same pattern as the suggestion.

code diff:

             if (item.has("publication_date")) {
-                entry.setField(StandardField.DATE, String.valueOf(item.optInt("publication_date")));
+                entry.setField(StandardField.DATE, item.optString("publication_date"));
+            } else if (item.has("publication_year")) {
+                entry.setField(StandardField.YEAR, item.optString("publication_year"));
             }

In jsonItemToBibEntry, change item.optInt("publication_date") to
item.optString("publication_date") to correctly parse the date, which is an ISO
8601 string from the OpenAlex API, not an integer.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [148-163]

     private BibEntry jsonItemToBibEntry(JSONObject item) throws ParseException {
         try {
             DoiCleanup DoiCleanup = new DoiCleanup();
             BibEntry entry = new BibEntry();
 
             entry.setType(EntryTypeFactory.parse(item.getString("type")));
 
             entry.setField(StandardField.TITLE, item.optString("title"));
 
             // TODO BibTeX vs. BibLaTeX
             if (item.has("publication_year")) {
                 entry.setField(StandardField.YEAR, String.valueOf(item.optInt("publication_year")));
             }
             if (item.has("publication_date")) {
-                entry.setField(StandardField.DATE, String.valueOf(item.optInt("publication_date")));
+                entry.setField(StandardField.DATE, item.optString("publication_date"));
             }
 ...
             DoiCleanup.cleanup(entry);
             return entry;
         } catch (JSONException e) {
             throw new ParseException("OpenAlex API JSON format has changed", e);
         }
     }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where publication_date is parsed as an integer instead of a string, which would lead to data loss. Applying this fix is critical for correctly importing entry data from the OpenAlex API.

Medium
Improve exception handling for URL parsing
Suggestion Impact:The commit refactored the getUrl method significantly, removing the try-catch block that caught MalformedURLException and re-threw it as IllegalArgumentException. Instead, the new implementation wraps the logic in a try-catch for RuntimeException and returns Optional.empty() on error. While not exactly matching the suggestion (which proposed letting MalformedURLException propagate), the commit does remove the problematic catch-and-rethrow pattern that obscured the original error, addressing the core issue identified in the suggestion.

code diff:

     private Optional<URL> getUrl(BibEntry entry, List<String> fieldsToSelect) throws MalformedURLException {
-        Optional<DOI> doiOpt = entry.getField(StandardField.DOI)
-                                    .flatMap(DOI::findInText);
-        if (doiOpt.isPresent()) {
-            return Optional.of(getUrl("/" + doiOpt.get().getURIAsASCIIString(), fieldsToSelect));
-        }
-        Optional<String> urlOpt = entry.getField(StandardField.URL);
-        if (urlOpt.isPresent()) {
-            String url = urlOpt.get();
-            String lower = url.toLowerCase();
-            try {
-                int idx = lower.indexOf("openalex.org/");
-                if (idx >= 0) {
-                    String tail = url.substring(idx + "openalex.org/".length());
-                    LOGGER.debug("URL for query: {}", tail);
-                    int queryIdx = tail.indexOf('?');
-                    if (queryIdx >= 0) {
-                        tail = tail.substring(0, queryIdx);
-                    }
-                    if (!tail.isBlank()) {
-                        return Optional.of(getUrl("/" + tail, fieldsToSelect));
-                    }
-                }
-            } catch (MalformedURLException ignored) {
-                throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
-            }
-        }
-        return Optional.empty();
-    }
-
-    private URL getUrl(String tail, List<String> fieldsToSelect) throws MalformedURLException {
+        try {
+            // Supported identifiers for lookup are listed at https://docs.openalex.org/api-entities/works/work-object#ids
+            return entry.getField(StandardField.DOI)
+                        .flatMap(DOI::findInText)
+                        .map(Unchecked.function(doi -> getUrl("/" + doi.getURIAsASCIIString(), fieldsToSelect)))
+
+                        .or(() -> entry.getField(StandardField.PMID)
+                                       // URL: See https://docs.openalex.org/api-entities/works/get-a-single-work#external-ids
+                                       .map(Unchecked.function(pmid -> getUrl("/pmid:" + pmid, fieldsToSelect))))
+
+                        // Fallback: OpenAlex identifier from URL
+                        .or(() -> entry.getField(StandardField.URL)
+                                       .flatMap(this::extractOpenAlexId)
+                                       .map(Unchecked.function(id -> getUrl("/" + id, fieldsToSelect))));
+        } catch (RuntimeException ignored) {
+            LOGGER.debug("Invalid OpenAlex URL");
+            return Optional.empty();
+        }
+    }

Improve exception handling in the getUrl method by removing the try-catch block
for MalformedURLException and allowing it to propagate, which aligns with the
method's signature and provides better error reporting.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [78-106]

     private Optional<URL> getUrl(BibEntry entry, List<String> fieldsToSelect) throws MalformedURLException {
         Optional<DOI> doiOpt = entry.getField(StandardField.DOI)
                                     .flatMap(DOI::findInText);
         if (doiOpt.isPresent()) {
             return Optional.of(getUrl("/" + doiOpt.get().getURIAsASCIIString(), fieldsToSelect));
         }
         Optional<String> urlOpt = entry.getField(StandardField.URL);
         if (urlOpt.isPresent()) {
             String url = urlOpt.get();
             String lower = url.toLowerCase();
-            try {
-                int idx = lower.indexOf("openalex.org/");
-                if (idx >= 0) {
-                    String tail = url.substring(idx + "openalex.org/".length());
-                    LOGGER.debug("URL for query: {}", tail);
-                    int queryIdx = tail.indexOf('?');
-                    if (queryIdx >= 0) {
-                        tail = tail.substring(0, queryIdx);
-                    }
-                    if (!tail.isBlank()) {
-                        return Optional.of(getUrl("/" + tail, fieldsToSelect));
-                    }
+            int idx = lower.indexOf("openalex.org/");
+            if (idx >= 0) {
+                String tail = url.substring(idx + "openalex.org/".length());
+                LOGGER.debug("URL for query: {}", tail);
+                int queryIdx = tail.indexOf('?');
+                if (queryIdx >= 0) {
+                    tail = tail.substring(0, queryIdx);
                 }
-            } catch (MalformedURLException ignored) {
-                throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
+                if (!tail.isBlank()) {
+                    return Optional.of(getUrl("/" + tail, fieldsToSelect));
+                }
             }
         }
         return Optional.empty();
     }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that catching MalformedURLException and re-throwing it as IllegalArgumentException obscures the original error, and removing the catch block improves error reporting as the method already declares throws MalformedURLException.

Low
High-level
Use a stable identifier for fetchers

The WebFetcher.getName() method serves a dual purpose for both display and as an
API key identifier, which is problematic for localization. It should be split
into a display name method and a separate, stable ID method for internal use.

Examples:

jablib/src/main/java/org/jabref/logic/importer/WebFetcher.java [18-22]
    /// TODO: This name is also used for identifying the fetcher in preferences for API keys.
    ///       Thus, any switch of the language will "remove" API keys.
    ///
    /// @return the localized name
    String getName();
jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [71]
        importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));

Solution Walkthrough:

Before:

// In WebFetcher.java
public interface WebFetcher {
    // Javadoc says this is a localized name
    String getName();
}

// In a fetcher implementation (e.g., OpenAlex.java)
public class OpenAlex implements WebFetcher, CustomizableKeyFetcher {
    @Override
    public String getName() {
        return "OpenAlex"; // Hardcoded string
    }

    private void someMethod() {
        // API key is fetched using the (potentially localized) name
        importerPreferences.getApiKey(getName()).ifPresent(...);
    }
}

After:

// In WebFetcher.java
public interface WebFetcher {
    // For display, can be localized
    String getName();

    // For internal use, stable and non-localized
    String getId();
}

// In a fetcher implementation (e.g., OpenAlex.java)
public class OpenAlex implements WebFetcher, CustomizableKeyFetcher {
    @Override
    public String getName() {
        return Localization.lang("OpenAlex"); // Localized
    }

    @Override
    public String getId() {
        return "openalex"; // Stable ID
    }

    private void someMethod() {
        // API key is fetched using the stable ID
        importerPreferences.getApiKey(getId()).ifPresent(...);
    }
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a design flaw where WebFetcher.getName() is used for both UI display and as a stable key for API secrets, which will break if names are localized; fixing this improves the architecture's robustness for internationalization.

Medium
Learned
best practice
Add null validation for parameters

Add null check for the entry parameter at the beginning of the method to prevent
potential NullPointerException and provide clearer error messages.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [318-333]

 public List<BibEntry> getReferences(BibEntry entry) throws FetcherException {
+    Objects.requireNonNull(entry, "BibEntry must not be null");
     try {
         return getWorkObject(entry, List.of("referenced_works"))
                 .map(work -> work.optJSONArray("referenced_works"))
                 .filter(Objects::nonNull)
                 .stream()
                 .flatMap(workUrlArray -> workUrlsToBibEntryStream(workUrlArray))
                 .toList();
     } catch (RuntimeException e) {
         ...
     }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Add input validation checks to methods that accept external data to ensure parameters are not null, empty, or invalid before processing, throwing appropriate exceptions when validation fails.

Low
Fix grammatical consistency in changelog
Suggestion Impact:The suggestion was directly implemented. Line 6 of the diff shows the exact change from "Citations Tab" to "the Citations tab" as suggested.

code diff:

-- We added support for selecting citation fetchers in Citations Tab. [#14430](https://github.com/JabRef/jabref/issues/14430)
+- We added support for selecting citation fetchers in the Citations tab. [#14430](https://github.com/JabRef/jabref/issues/14430)

Change "Citations Tab" to "the Citations tab" for consistency with standard
English grammar and documentation style.

CHANGELOG.md [18]

-- We added support for selecting citation fetchers in Citations Tab. [#14430](https://github.com/JabRef/jabref/issues/14430)
+- We added support for selecting citation fetchers in the Citations tab. [#14430](https://github.com/JabRef/jabref/issues/14430)

[Suggestion processed]

Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Fix typographical errors in comments, documentation, changelog entries, and user-facing strings to maintain professionalism and clarity.

Low
  • Update

@koppor koppor added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 4, 2026
@koppor koppor marked this pull request as draft February 4, 2026 08:23
| [Elsevier (ScienceDirect / Scopus)](https://dev.elsevier.com/) | [Elsevier Dev Portal](https://dev.elsevier.com/) | `ScopusApiKey` | [20.000 calls/week](https://dev.elsevier.com/api_key_settings.html) |
| [IEEEXplore](https://docs.jabref.org/collect/import-using-online-bibliographic-database#ieeexplore) | [IEEE Xplore API portal](https://developer.ieee.org) | `IEEEAPIKey` | 200 calls/day |
| [Medline/Pubmed](https://pubmed.ncbi.nlm.nih.gov/) | [NCBI User account](https://account.ncbi.nlm.nih.gov/settings/) | `medlineApiKey` | 10 requests/seconds |
| [MathSciNet](http://www.ams.org/mathscinet) | (none) | (none) | Depending on the current network |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚫 [linkspector] reported by reviewdog 🐶
Cannot reach http://www.ams.org/mathscinet Status: null net::ERR_BLOCKED_BY_CLIENT at http://www.ams.org/mathscinet

String url = urlOpt.get();
String lower = url.toLowerCase();
try {
int idx = lower.indexOf("openalex.org/");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wrap in an URL Objet and use getHost

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

or also getQuery to check

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note: approved in #14020 - I just moved the code.

entry.setField(StandardField.TITLE, item.optString("title"));

// Year
// TODO BibTeX vs. BibLaTeX
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can put both in Date. We have a post cleanup for every fetcher that converts based on library mode

fetcher = new OpenAlex();
ImporterPreferences importerPreferences = mock(ImporterPreferences.class);
when(importerPreferences.getApiKeys()).thenReturn(FXCollections.emptyObservableSet());
when(importerPreferences.getApiKey(MedlineFetcher.FETCHER_NAME)).thenReturn(API_KEY);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Medline fetcher with open alex key does not make sense

@koppor koppor marked this pull request as ready for review February 4, 2026 10:32
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
API key exposure

Description: API key is passed as URL query parameter, which may be logged in server logs, proxy logs,
or browser history, potentially exposing the credential.
OpenAlex.java [70-70]

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
uriBuilder.addParameter("search", new DefaultQueryTransformer().transformSearchQuery(queryNode).orElse(""));
API key exposure

Description: API key is passed as URL query parameter in multiple locations, which may be logged in
server logs, proxy logs, or browser history, potentially exposing the credential.
OpenAlex.java [110-110]

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
String fieldList = fieldsToSelect.stream()
Information disclosure

Description: Empty catch block silently ignores MalformedURLException, then throws
IllegalArgumentException with the original URL in the message, which could expose
sensitive information in error logs.
OpenAlex.java [100-100]

Referred Code
} catch (MalformedURLException ignored) {
    throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
Data integrity issue

Description: Creating a BibEntry with a redacted URL and marking it as changed when fetching fails
could lead to data corruption or unexpected behavior in the application state.
OpenAlex.java [315-315]

Referred Code
    return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
} catch (RuntimeException e) {
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
API Key Logging: API keys are added as URL parameters and logged via LOGGER.debug("URL for query:
{}", result) which may expose sensitive credentials in logs.

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
uriBuilder.addParameter("search", new DefaultQueryTransformer().transformSearchQuery(queryNode).orElse(""));
URL result = uriBuilder.build().toURL();
LOGGER.debug("URL for query: {}", result);
return result;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
URL Parameter Injection: API keys from user preferences are directly added to URL parameters without validation,
which could allow injection if the key contains malicious characters.

Referred Code
importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));
uriBuilder.addParameter("search", new DefaultQueryTransformer().transformSearchQuery(queryNode).orElse(""));

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Feb 4, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Use a non-localized fetcher identifier

The WebFetcher.getName() method serves a dual purpose for both UI display and as
a preference key, which is problematic because it's localized. To fix this, a
new non-localized getIdentifier() method should be added for use as a stable
key.

Examples:

jablib/src/main/java/org/jabref/logic/importer/WebFetcher.java [15-22]
    /// Returns the localized name of this fetcher.
    /// The title can be used to display the fetcher in the menu and in the side pane.
    ///
    /// TODO: This name is also used for identifying the fetcher in preferences for API keys.
    ///       Thus, any switch of the language will "remove" API keys.
    ///
    /// @return the localized name
    String getName();
jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [63-70]
    public String getName() {
        return FETCHER_NAME;
    }

    @Override
    public URL getURLForQuery(BaseQueryNode queryNode) throws URISyntaxException, MalformedURLException {
        URIBuilder uriBuilder = new URIBuilder(URL_PATTERN);
        importerPreferences.getApiKey(FETCHER_NAME).ifPresent(apiKey -> uriBuilder.addParameter("api_key", apiKey));

Solution Walkthrough:

Before:

public interface WebFetcher {
    // This is used for both UI display and as a key for API keys.
    // Javadoc says it's localized.
    String getName();
}

public class OpenAlex implements WebFetcher {
    public static final String FETCHER_NAME = "OpenAlex";

    @Override
    public String getName() {
        return FETCHER_NAME;
    }

    public void someMethod() {
        // API key is fetched using the (potentially localized) name.
        importerPreferences.getApiKey(FETCHER_NAME);
    }
}

After:

public interface WebFetcher {
    // For UI display, can be localized.
    String getName();

    // For internal identification and preference keys, non-localized.
    String getIdentifier();
}

public class OpenAlex implements WebFetcher {
    @Override
    public String getName() {
        return "OpenAlex"; // Or a localized string
    }

    @Override
    public String getIdentifier() {
        return "OpenAlex"; // A constant, non-localized ID
    }

    public void someMethod() {
        // API key is fetched using the non-localized identifier.
        importerPreferences.getApiKey(this.getIdentifier());
    }
}
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a significant design flaw where a localized name is used as a persistent key, which could lead to a poor user experience. The proposed solution is a standard and robust architectural improvement.

Medium
General
Batch API requests to improve performance

Optimize workUrlsToBibEntryStream by batching API requests to fetch multiple
works at once. This avoids the N+1 problem, improving performance and reducing
the number of API calls.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [296-322]

 private Stream<BibEntry> workUrlsToBibEntryStream(JSONArray workUrlArray) {
-    return IntStream.range(0, workUrlArray.length())
-                    .mapToObj(workUrlArray::getString)
-                    .map(Unchecked.function(workUrl -> getUrl("/" + workUrl, List.of())))
-                    .map(Unchecked.function(url -> {
+    List<String> workIds = IntStream.range(0, workUrlArray.length())
+                                    .mapToObj(workUrlArray::getString)
+                                    .map(url -> url.substring(url.lastIndexOf('/') + 1))
+                                    .toList();
+
+    // The API seems to have a limit on URL length, so we batch the requests.
+    // A batch size of 50 seems to be a safe value.
+    int batchSize = 50;
+    return IntStream.range(0, (workIds.size() + batchSize - 1) / batchSize)
+                    .mapToObj(i -> workIds.subList(i * batchSize, Math.min((i + 1) * batchSize, workIds.size())))
+                    .flatMap(Unchecked.function(batch -> {
+                        String filterValue = String.join("|", batch);
+                        URIBuilder uriBuilder = getUriBuilder("", List.of())
+                                .addParameter("filter", "openalex_id:" + filterValue);
+                        URL url = uriBuilder.build().toURL();
                         try (ProgressInputStream stream = getUrlDownload(url).asInputStream()) {
-                            return jsonItemToBibEntry(JsonReader.toJsonObject(stream));
-                        } catch (FetcherClientException e) {
-                            String redactedUrl = FetcherException.getRedactedUrl(url.toString());
-                            if (e.getHttpResponse().isPresent()) {
-                                int code = e.getHttpResponse().get().statusCode();
-                                if (code == 404) {
-                                    LOGGER.trace("Work not found at URL: {}", redactedUrl);
-                                } else {
-                                    LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
-                                }
-                            } else {
-                                LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
-                            }
-                            return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
-                        } catch (RuntimeException e) {
-                            String redactedUrl = FetcherException.getRedactedUrl(url.toString());
-                            LOGGER.debug("Could not fetch work at URL: {}", redactedUrl, e);
-                            return new BibEntry().withField(StandardField.URL, redactedUrl).withChanged(true);
+                            JSONObject response = JsonReader.toJsonObject(stream);
+                            JSONArray results = response.getJSONArray("results");
+                            return workArrayToBibEntryStream(results);
+                        } catch (Exception e) {
+                            LOGGER.warn("Could not fetch batch of works", e);
+                            return Stream.empty();
                         }
                     }));
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential N+1 problem and proposes an effective solution using batch API requests. This significantly improves performance and efficiency, especially when dealing with entries that have many references.

Medium
Improve identifier resolution for API
Suggestion Impact:The commit partially implements the suggestion's intent but takes a different approach. Instead of adding OPENALEXID field support, it adds PMID field support and creates a dedicated extractOpenAlexId method for URL parsing. The commit does refactor the getUrl method to use a cleaner chained Optional pattern and improves URL parsing, but doesn't use DOI.findInText for OpenAlex URLs as suggested. The prioritization logic is similar (DOI first, then other identifiers, then URL fallback) but the specific implementation differs.

code diff:

+    @VisibleForTesting
+    Optional<String> extractOpenAlexId(String url) {
+        try {
+            URL u = new URL(url);
+
+            if (!"openalex.org".equalsIgnoreCase(u.getHost())) {
+                return Optional.empty();
+            }
+
+            String path = u.getPath();          // "/works/W4408614692" or "/W4408614692"
+            if (path.isBlank() || "/".equals(path)) {
+                return Optional.empty();
+            }
+
+            String[] segments = path.split("/");
+            String id = segments[segments.length - 1];
+
+            return id.isBlank() ? Optional.empty() : Optional.of(id);
+        } catch (MalformedURLException e) {
+            return Optional.empty();
+        }
+    }
+
     @Override
     public URL getURLForQuery(BaseQueryNode queryNode) throws URISyntaxException, MalformedURLException {
         URIBuilder uriBuilder = new URIBuilder(URL_PATTERN);
@@ -75,33 +99,24 @@
     }
 
     private Optional<URL> getUrl(BibEntry entry, List<String> fieldsToSelect) throws MalformedURLException {
-        Optional<DOI> doiOpt = entry.getField(StandardField.DOI)
-                                    .flatMap(DOI::findInText);
-        if (doiOpt.isPresent()) {
-            return Optional.of(getUrl("/" + doiOpt.get().getURIAsASCIIString(), fieldsToSelect));
-        }
-        Optional<String> urlOpt = entry.getField(StandardField.URL);
-        if (urlOpt.isPresent()) {
-            String url = urlOpt.get();
-            String lower = url.toLowerCase();
-            try {
-                int idx = lower.indexOf("openalex.org/");
-                if (idx >= 0) {
-                    String tail = url.substring(idx + "openalex.org/".length());
-                    LOGGER.debug("URL for query: {}", tail);
-                    int queryIdx = tail.indexOf('?');
-                    if (queryIdx >= 0) {
-                        tail = tail.substring(0, queryIdx);
-                    }
-                    if (!tail.isBlank()) {
-                        return Optional.of(getUrl("/" + tail, fieldsToSelect));
-                    }
-                }
-            } catch (MalformedURLException ignored) {
-                throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
-            }
-        }
-        return Optional.empty();
+        try {
+            // Supported identifiers for lookup are listed at https://docs.openalex.org/api-entities/works/work-object#ids
+            return entry.getField(StandardField.DOI)
+                        .flatMap(DOI::findInText)
+                        .map(Unchecked.function(doi -> getUrl("/" + doi.getURIAsASCIIString(), fieldsToSelect)))
+
+                        .or(() -> entry.getField(StandardField.PMID)
+                                       // URL: See https://docs.openalex.org/api-entities/works/get-a-single-work#external-ids
+                                       .map(Unchecked.function(pmid -> getUrl("/pmid:" + pmid, fieldsToSelect))))
+
+                        // Fallback: OpenAlex identifier from URL
+                        .or(() -> entry.getField(StandardField.URL)
+                                       .flatMap(this::extractOpenAlexId)
+                                       .map(Unchecked.function(id -> getUrl("/" + id, fieldsToSelect))));
+        } catch (RuntimeException ignored) {
+            LOGGER.debug("Invalid OpenAlex URL");
+            return Optional.empty();
+        }
     }

Refactor the getUrl method to prioritize the OPENALEXID field for API lookups.
Additionally, simplify the parsing of OpenAlex URLs by using the existing
DOI.findInText utility.

jablib/src/main/java/org/jabref/logic/importer/fetcher/OpenAlex.java [77-105]

 private Optional<URL> getUrl(BibEntry entry, List<String> fieldsToSelect) throws MalformedURLException {
+    // Prefer OpenAlex ID if available
+    Optional<String> openAlexId = entry.getField(StandardField.OPENALEXID);
+    if (openAlexId.isPresent()) {
+        return Optional.of(getUrl("/" + openAlexId.get(), fieldsToSelect));
+    }
+
+    // Then try DOI
     Optional<DOI> doiOpt = entry.getField(StandardField.DOI)
                                 .flatMap(DOI::findInText);
     if (doiOpt.isPresent()) {
         return Optional.of(getUrl("/" + doiOpt.get().getURIAsASCIIString(), fieldsToSelect));
     }
+
+    // Finally, look for an OpenAlex URL
     Optional<String> urlOpt = entry.getField(StandardField.URL);
     if (urlOpt.isPresent()) {
-        String url = urlOpt.get();
-        String lower = url.toLowerCase();
-        try {
-            int idx = lower.indexOf("openalex.org/");
-            if (idx >= 0) {
-                String tail = url.substring(idx + "openalex.org/".length());
-                LOGGER.debug("URL for query: {}", tail);
-                int queryIdx = tail.indexOf('?');
-                if (queryIdx >= 0) {
-                    tail = tail.substring(0, queryIdx);
-                }
-                if (!tail.isBlank()) {
-                    return Optional.of(getUrl("/" + tail, fieldsToSelect));
-                }
-            }
-        } catch (MalformedURLException ignored) {
-            throw new IllegalArgumentException("Invalid OpenAlex URL: " + url);
+        // DOI.findInText can also parse openalex.org URLs and return a DOI object
+        Optional<DOI> openAlexDOI = DOI.findInText(urlOpt.get());
+        if (openAlexDOI.isPresent()) {
+            return Optional.of(getUrl("/" + openAlexDOI.get().getDOI(), fieldsToSelect));
         }
     }
     return Optional.empty();
 }

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that using a dedicated OPENALEXID field would be more robust and proposes a good refactoring to simplify URL parsing, improving code clarity and maintainability.

Medium
Learned
best practice
Handle initialization errors properly

The MVStore initialization in the constructor lacks proper error handling when
IOException occurs. The mvStore field could remain null if file operations fail,
leading to NullPointerException in the close() method.

jablib/src/main/java/org/jabref/logic/citation/repository/BibEntryCitationsAndReferencesRepositoryShell.java [38-60]

 @Nullable
 private final MVStore mvStore;
 
 public BibEntryCitationsAndReferencesRepositoryShell(Path citationsRelationsDirectory,
                                                      int storeTTL,
                                                      ImportFormatPreferences importFormatPreferences,
                                                      FieldPreferences fieldPreferences,
                                                      BibEntryTypesManager entryTypesManager,
                                                      ObjectProperty<CitationFetcherType> citationFetcherTypeProperty) {
     Path storePath = citationsRelationsDirectory.resolve(CITATION_RELATIONS_STORE);
     try {
         Files.createDirectories(storePath.getParent());
         if (!Files.exists(storePath)) {
             Files.createFile(storePath);
         }
+        mvStore = new MVStore.Builder()
+                .fileName(storePath.toAbsolutePath().toString())
+                .open();
     } catch (IOException e) {
         LOGGER.error("An error occurred while opening {} storage", storePath, e);
+        throw new RuntimeException("Failed to initialize MVStore", e);
     }
-    
-    mvStore = new MVStore.Builder()
-            .fileName(storePath.toAbsolutePath().toString())
-            .open();
     ...
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Implement thread-safe initialization patterns for static fields that are lazily initialized, using volatile modifiers and double-checked locking to prevent race conditions in multi-threaded environments.

Low
  • More

@koppor koppor mentioned this pull request Feb 4, 2026
4 tasks
Comment thread CHANGELOG.md Outdated
Siedlerchr
Siedlerchr previously approved these changes Feb 4, 2026
Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>
@koppor koppor added the automerge PR is tagged with that label will be merged if workflows are green label Feb 4, 2026
@koppor koppor enabled auto-merge February 4, 2026 11:15
@koppor koppor removed the automerge PR is tagged with that label will be merged if workflows are green label Feb 4, 2026
@koppor koppor added this pull request to the merge queue Feb 4, 2026
@github-actions github-actions Bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Feb 4, 2026
Merged via the queue into main with commit 9e9e0e4 Feb 4, 2026
59 of 67 checks passed
@koppor koppor deleted the add-open-alex branch February 4, 2026 11:57
Siedlerchr added a commit that referenced this pull request Feb 4, 2026
* upstream/main:
  New Crowdin updates (#15035)
  Use patched Gradle version (#15034)
  Add OpenAlex-based Citation Fetcher (#15023)
  Update null annotaitons at EntryBasedFetcher (#15024)
  Fix CHANGELOG.md test
  Use _ for unused variables (#15028)
  Use ubuntu-latest for checkstyle and javadoc
  Update Gradle Wrapper from 9.3.0-jabref-2 to 9.3.1 (#15021)
  Use "ubuntu-slim" for most workflows (#15019)
  Refine GroupsTree (#15013)
Siedlerchr added a commit to Jalina2007/jabref that referenced this pull request Feb 5, 2026
…4902

* upstream/main: (23 commits)
  Some more recipes from OpenRewrite (JabRef#15030)
  feat: Add PDF Upload endpoint to EntryResource (JabRef#14963)
  Heuristics also used at batch (JabRef#15025)
  Fix cleanup-pr.yml
  New Crowdin updates (JabRef#15035)
  Use patched Gradle version (JabRef#15034)
  Add OpenAlex-based Citation Fetcher (JabRef#15023)
  Update null annotaitons at EntryBasedFetcher (JabRef#15024)
  Fix CHANGELOG.md test
  Use _ for unused variables (JabRef#15028)
  Use ubuntu-latest for checkstyle and javadoc
  Update Gradle Wrapper from 9.3.0-jabref-2 to 9.3.1 (JabRef#15021)
  Use "ubuntu-slim" for most workflows (JabRef#15019)
  Refine GroupsTree (JabRef#15013)
  New Crowdin updates (JabRef#15018)
  Added Clear group option (JabRef#15017)
  Chore(deps): Bump com.uber.nullaway:nullaway from 0.12.15 to 0.13.1 in /versions (JabRef#15006)
  Chore(deps): Bump tools.jackson:jackson-bom in /versions (JabRef#15007)
  No rush in Docker building
  Yaml issue workaround
  ...
Siedlerchr added a commit that referenced this pull request Feb 8, 2026
…es/jablib/src/main/resources/csl-styles-6c79ffe

* upstream/main: (68 commits)
  Chore(deps): Bump org.apache.httpcomponents.client5:httpclient5 (#15060)
  Chore(deps): Bump com.google.errorprone:error_prone_core in /versions (#15059)
  Chore(deps): Bump de.undercouch.download:de.undercouch.download.gradle.plugin (#15057)
  Chore(deps): Bump org.postgresql:postgresql in /versions (#15058)
  Chore(deps): Bump de.undercouch.download:de.undercouch.download.gradle.plugin (#15056)
  Updates on Wednesday, not on Sunday
  Add screenshot requirement (#15050)
  Switch image for javadoc
  Better docker layer caching during build (#15042)
  New Crowdin updates (#15045)
  Chore: reuse shared 'setup-gradle' in all places in test-code.yml (#15043)
  Chore: add 'testlens-app/setup-testlens' GH action (#15044)
  Add: HTTP Server and LSP server toggles to quick settings (#14972)
  Some more recipes from OpenRewrite (#15030)
  feat: Add PDF Upload endpoint to EntryResource (#14963)
  Heuristics also used at batch (#15025)
  Fix cleanup-pr.yml
  New Crowdin updates (#15035)
  Use patched Gradle version (#15034)
  Add OpenAlex-based Citation Fetcher (#15023)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: fetcher Review effort 4/5 status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers status: to-be-merged PRs which are accepted and should go into the merge-queue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants