Skip to content

JabKit: Add file output option to cititation commands (and refactoring)#15913

Open
JohnnyGoodNews wants to merge 45 commits into
JabRef:mainfrom
JohnnyGoodNews:feat/jabkit-cli-cititation-output
Open

JabKit: Add file output option to cititation commands (and refactoring)#15913
JohnnyGoodNews wants to merge 45 commits into
JabRef:mainfrom
JohnnyGoodNews:feat/jabkit-cli-cititation-output

Conversation

@JohnnyGoodNews

@JohnnyGoodNews JohnnyGoodNews commented Jun 7, 2026

Copy link
Copy Markdown

Related issues and pull requests

Closes


None, just an adoption of the existing output format capability for the get-cited/citing-works Command in JabKit CLI.

PR Description

The added feature is a result of a discussion with @koppor in the search for a good first issue/feature for me to get a better grasp on the codebase. However I would enjoy feedback on the implementation and patterns.

The implementation adds the options for file output and file format to the Commands get-cited-works and get-citing-works.

Two main refactorings/patterns emerged for me when I struggled with unit testing and comprehension due to quite some boilerplate (if-else and try-catches) in the commands:

  1. Centralized functionality by extraction of a service (or facade) layer for import/export functionality (previously mostly clustered as public static methods in JabKit) - see ADR-61
  2. Introduction of CliExceptions and CliExceptionHandler to wrap errors and their exit codes to reduce the amount of try-catch blocks that can't really be handled anyways - see ADR-62

Intent: Better 3rd party tools integration & workflows by supporting multiple output formats. (The Impact of the feature might be still low until some concrete requirements for integration are established - read: I'm not sure how users are integrating JabKit already)

The Impact of the refactoring is a decrease of duplications and it got a bit easier to read the actual flow in the commands. Also some commands had subtly different behavior which should be better now. I was able to reduce the amount of Optional/Either-type return values in favor of plain return types and Exception in error cases (I might be biased here in my preference).

Screenshot of different output formats:
JabKit-citations-output-files

Single entry library:
single-entry-library

jabref-contrib-policy:4.2:reviewed​:ok

Steps to test

get-cited-works: ./gradlew :jabkit:run --args="get-cited-works 10.1109/ICSA59870.2024.00011 --output=get-cited-works.csv --output-format=CSV"
get-citing-works: ./gradlew :jabkit:run --args="get-citing-works 10.1109/ICSA59870.2024.00011 --output=get-citing-works.html --output-format=html"
(and check the resulting output files)

AI usage


Claude Code: (mostly to challenge my ideas for simpler solutions)

  • model claude-opus-4-6 for architecture feedback; checking code against docs/adr
  • model claude-sonnet-4-6 and gpt-codex-5.3 for test code and comparison implementations (not committed/discarded)

no AI generated code in this PR

Checklist

  • I own the copyright of the code submitted and I license it under the [MIT license](https://github.com/JabRef/jabref/blob/main/LICENSE)
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • [/] I manually tested my changes in running JabRef (always required)
    • only tested directly calling the JabKit class with arg-vector but not with a bundled os binary (I might need help there)
  • I added JUnit tests for changes (if applicable)
  • I added screenshots in the PR description (if change is visible to the user)
  • I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • I checked the [user documentation](https://docs.jabref.org/) for up to dateness and submitted a pull request to our [user documentation repository](https://github.com/JabRef/user-documentation/tree/main/en)

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Hey @JohnnyGoodNews! 👋

Thank you for contributing to JabRef!

We have automated checks in place, based on which you will soon get feedback if any of them are failing. We also use Qodo for review assistance. It will update your pull request description with a review help and offer suggestions to improve the pull request.

After all automated checks pass, a maintainer will also review your contribution. Once that happens, you can go through their comments in the "Files changed" tab and act on them, or reply to the conversation if you have further inputs. You can read about the whole pull request process in our contribution guide.

Please ensure that your pull request is in line with our AI Usage Policy and make necessary disclosures.

@koppor koppor added dev: code-quality Issues related to code or architecture decisions component: JabKit [cli] labels Jun 7, 2026
@@ -0,0 +1,39 @@
---
nav_order: 0061

@subhramit subhramit Jun 7, 2026

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.

Suggested change
nav_order: 0061
nav_order: 0062

Because probably #15907 will be merged first.
(Please also change filename)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, good catch. I didn't see that PR.

@koppor koppor left a comment

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.

I reviewed with RefactoringMiner. Looks good. Just small comments.

Maybe use @Nullmarked at CliExceptionHandler and other (short) classes instead of one @NonNull.

docker run --pull always --rm -p 6789:6789  -v c:\users\koppor\rmc:/diff tsantalis/refactoringminer diff --url
https://github.com/JabRef/jabref/pull/15913

Comment thread docs/decisions/0063-use-picocli-exceptionhandler.md Outdated

Commands rarely catch exceptions or check for valid return values. Exceptions are thrown as `CliException` and subclasses (or `JabRefException`) from the service and helper classes but not handled in the command.

## Pros and Cons of the Options

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.

For a short ADR, you can just use "Decision Outcome" (and maybe the Consequences) to "discuss"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It was redundant too - I took out the pro/con of the alternatives.. I left the Confirmation in though, because that was helpful for my review agent.

return Math.max(consistencyExit, integrityExit);
}

private int tryIntegrityCheck() {

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.

I would not prefix with "try" - why not just integrityCheck (or runIntegrityCheck)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The idea is to handle the exceptions and continue processing. Maybe handleIntegrityCheck or runAndContinueIntegrityCheck? (If there were 3+ subcommands I would probably use something like runAndHandle(ThrowingIntFunction subcommand) - but that seemed overly complex)

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.

checkIntegrity?
(try-catch is a common way of handling, it doesn't need to be documented in method names)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I followed your suggestions 👍.

It's probably just my old habit of wrapping impure functions and prefixing with tryFoo when it's only about the exception handling/reducing to allowed ranges.

@JohnnyGoodNews JohnnyGoodNews requested a review from koppor June 7, 2026 13:58
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0)

Grey Divider


Action required

1. ExportService prints hardcoded text ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
ExportService and the new Picocli options (--output, --output-format) emit hardcoded,
non-localized English user-facing text, and ExportService also constructs a localized warning via
string concatenation. This prevents translation and breaks placeholder-based localization formatting
required for CLI output and help text.
Code

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[R127-146]

+        if (!FileUtil.isBibFile(outputFile)) {
+            System.err.println(Localization.lang("Invalid output file type provided."));
+        }
+        try (AtomicFileWriter fileWriter = new AtomicFileWriter(outputFile, StandardCharsets.UTF_8)) {
+            BibWriter bibWriter = new BibWriter(fileWriter, OS.NEWLINE);
+            SelfContainedSaveConfiguration saveConfiguration = (SelfContainedSaveConfiguration) new SelfContainedSaveConfiguration()
+                    .withReformatOnSave(cliPreferences.getLibraryPreferences().shouldAlwaysReformatOnSave());
+            BibDatabaseWriter databaseWriter = new BibDatabaseWriter(
+                    bibWriter,
+                    saveConfiguration,
+                    cliPreferences.getFieldPreferences(),
+                    cliPreferences.getCitationKeyPatternPreferences(),
+                    entryTypesManager);
+            databaseWriter.writeDatabase(bibDatabaseContext);
+
+            // Show just a warning message if encoding did not work for all characters:
+            if (fileWriter.hasEncodingProblems()) {
+                System.err.println(Localization.lang("Warning") + ": "
+                        + Localization.lang("UTF-8 could not be used to encode the following characters: %0", fileWriter.getEncodingProblems()));
+            }
Evidence
The compliance localization rules require all user-facing CLI strings (including Picocli help/usage
text) to be routed through Localization.lang(...) and to use single localized templates with
placeholders rather than concatenating fragments. The cited ExportService lines show it printing a
raw English error and assembling the UTF-8 warning by concatenating localized pieces, while the
cited Picocli command classes define description values for --output and --output-format as
hardcoded English strings; the referenced JabRef_en.properties file is the expected location for
the corresponding message keys.

AGENTS.md: Localization: Localize All User-Facing Text; Keep Logs in English; Only Edit JabRef_en.properties: AGENTS.md: Localization: Localize All User-Facing Text; Keep Logs in English; Only Edit JabRef_en.properties: AGENTS.md: Localization: Localize All User-Facing Text; Keep Logs in English; Only Edit JabRef_en.properties: AGENTS.md: Localization: Localize All User-Facing Text; Keep Logs in English; Only Edit JabRef_en.properties
jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[127-129]
jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[142-146]
jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[37-48]
jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[37-48]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ExportService` prints user-facing messages that are not localized and builds localized output by concatenating strings, and the Picocli option help texts for `--output` and `--output-format` are hardcoded in English.
## Issue Context
Compliance requires all user-facing CLI text (including Picocli help/usage output) to go through `Localization.lang(...)` and to avoid constructing localized messages by concatenating strings; instead, define a single localized message with placeholders and store the English defaults in `jablib/src/main/resources/l10n/JabRef_en.properties`.
## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[127-129]
- jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[142-146]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[37-48]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[37-48]
- jablib/src/main/resources/l10n/JabRef_en.properties[328-334]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. ExportServiceTest catches exceptions ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test catches ExportServiceException via a manual try/catch block instead of using JUnit’s
assertThrows. This violates the project’s test conventions and can reduce clarity of test intent.
Code

jabkit/src/test/java/org/jabref/toolkit/service/ExportServiceTest.java[R58-72]

+    @Test
+    void wrongOutputFormatFails(@TempDir Path tempDir) throws Exception {
+        try {
+            Path source = getClassResourceAsPath("origin.bib").toAbsolutePath();
+            ParserResult parserResult = ImportService.importBibTexFile(source, preferences, true);
+            Path output = tempDir.resolve("output.bibtex");
+
+            String invalidFormat = "Klingon";
+            new ExportService(preferences).exportParserResultToFile(parserResult, output, invalidFormat, true);
+
+            fail("An ExportServiceException should have been thrown");
+        } catch (ExportServiceException e) {
+            assertTrue(e.getMessage().contains("format"));
+        }
+    }
Evidence
Rule 20 disallows catching exceptions in tests. wrongOutputFormatFails uses a try/catch to assert
the exception type/message instead of assertThrows.

AGENTS.md: JUnit/Test Conventions: Avoid @DisplayName, Prefer @TempDir, Use Direct Assertions, and Do Not Catch Exceptions in Tests: AGENTS.md: JUnit/Test Conventions: Avoid @DisplayName, Prefer @TempDir, Use Direct Assertions, and Do Not Catch Exceptions in Tests: AGENTS.md: JUnit/Test Conventions: Avoid @DisplayName, Prefer @TempDir, Use Direct Assertions, and Do Not Catch Exceptions in Tests: AGENTS.md: JUnit/Test Conventions: Avoid @DisplayName, Prefer @TempDir, Use Direct Assertions, and Do Not Catch Exceptions in Tests
jabkit/src/test/java/org/jabref/toolkit/service/ExportServiceTest.java[58-72]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A unit test catches exceptions with a `try/catch` instead of using `assertThrows`.
## Issue Context
Project test conventions require not catching exceptions in tests; JUnit should handle them and assertions should be expressed via `assertThrows`.
## Fix Focus Areas
- jabkit/src/test/java/org/jabref/toolkit/service/ExportServiceTest.java[58-72]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Check ignores findings code ✓ Resolved 🐞 Bug ≡ Correctness
Description
Check.checkConsistency() and Check.checkIntegrity() ignore the int returned by
CheckConsistency.execute(...) / CheckIntegrity.execute(...) and always return ExitCode.OK when
no exception is thrown, so jabkit check FILE can exit 0 even when findings exist (which should
exit 1).
Code

jabkit/src/main/java/org/jabref/toolkit/commands/Check.java[R61-78]

+    private int checkIntegrity() {
+        try {
+            CheckIntegrity.execute(inputFile, outputFormat, true, sharedOptions.porcelain, jabKit);
+            return CommandLine.ExitCode.OK;
+        } catch (ImportServiceException e) {
+            System.err.println(e.getLocalizedMessage());
+            return e.getExitCode();
+        }
+    }
+
+    private int checkConsistency() {
+        try {
+            CheckConsistency.execute(inputFile, outputFormat, sharedOptions.porcelain, jabKit);
+            return CommandLine.ExitCode.OK;
+        } catch (ImportServiceException e) {
+            System.err.println(e.getLocalizedMessage());
+            return e.getExitCode();
+        }
Evidence
CheckConsistency.execute(...)/CheckIntegrity.execute(...) return 1 when findings exist, but
Check.checkConsistency()/checkIntegrity() always return ExitCode.OK after calling them, so
findings are never surfaced in the parent command’s exit code.

jabkit/src/main/java/org/jabref/toolkit/commands/Check.java[61-79]
jabkit/src/main/java/org/jabref/toolkit/commands/CheckConsistency.java[54-73]
jabkit/src/main/java/org/jabref/toolkit/commands/CheckConsistency.java[128-136]
jabkit/src/main/java/org/jabref/toolkit/commands/CheckIntegrity.java[64-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Check.checkConsistency()` and `Check.checkIntegrity()` currently discard the return value of the underlying `execute(...)` methods and always return `ExitCode.OK` on success.
This breaks the CLI contract that `jabkit check <file>` should exit with `1` when findings exist (and `0` only when clean).
## Issue Context
Both `CheckConsistency.execute(...)` and `CheckIntegrity.execute(...)` still return `int` exit codes indicating findings vs. clean; the wrapper methods must propagate those values.
## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/commands/Check.java[61-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. ChatModel resource leak 🐞 Bug ☼ Reliability
Description
CitationFetcherFactory.getCitationFetcher(...) creates a ChatModel but never closes it; since
ChatModel is AutoCloseable, repeated use can leak resources such as threads or HTTP connections.
Code

jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java[R24-35]

+    public CitationFetcher getCitationFetcher(CitationFetcherType citationFetcherType) {
+        ChatModel chatModel = ChatModelFactory.create(cliPreferences.getAiPreferences());
+        return CitationFetcherType.getCitationFetcher(
+                citationFetcherType,
+                cliPreferences.getImporterPreferences(),
+                cliPreferences.getImportFormatPreferences(),
+                cliPreferences.getCitationKeyPatternPreferences(),
+                cliPreferences.getGrobidPreferences(),
+                cliPreferences.getAiPreferences(),
+                chatModel
+        );
+    }
Evidence
The factory allocates a ChatModel each time getCitationFetcher is called and returns a fetcher
without any closing path. ChatModel is explicitly AutoCloseable, so not closing it is a leak.

jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java[24-35]
jablib/src/main/java/org/jabref/logic/ai/chatting/ChatModel.java[7-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CitationFetcherFactory.getCitationFetcher(...)` constructs a `ChatModel` but does not close it. `ChatModel` implements `AutoCloseable`, so this is a resource leak.
## Issue Context
Previously, commands used try-with-resources around `ChatModel`. After refactoring, the lifecycle is lost.
A robust fix is to return a handle/wrapper that implements `AutoCloseable` and closes the `ChatModel` (and any other resources) when done, and to use it with try-with-resources in `GetCitedWorks`/`GetCitingWorks`.
## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java[24-35]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[60-80]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[60-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Export logs break porcelain ✓ Resolved 🐞 Bug ≡ Correctness
Description
ExportService.tryExportWithExporter(...) always prints an "Exporting …" line, which (a) produces
duplicate output because exportParserResultToFile(...) already prints, and (b) emits status text
even when callers request --porcelain output.
Code

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[R173-210]

+    public void exportParserResultToFile(
+            ParserResult parserResult,
+            Path outputFile,
+            String format,
+            boolean porcelain) throws ExportServiceException {
+
+        if (!porcelain) {
+            System.out.println(Localization.lang("Exporting '%0'.", outputFile));
+        }
+
+        Optional<Path> path = parserResult.getPath().map(Path::toAbsolutePath);
+        BibDatabaseContext databaseContext = parserResult.getDatabaseContext();
+        path.ifPresent(databaseContext::setDatabasePath);
+        List<Path> fileDirForDatabase = databaseContext
+                .getFileDirectories(cliPreferences.getFilePreferences());
+
+        List<BibEntry> entries = databaseContext.getDatabase().getEntries();
+
+        Exporter exporter = getExporterByName(format);
+        tryExportWithExporter(exporter, outputFile, databaseContext, entries, fileDirForDatabase);
+    }
+
+    private static void tryExportWithExporter(
+            Exporter exporter,
+            Path outputFile,
+            BibDatabaseContext databaseContext,
+            List<BibEntry> entries,
+            List<Path> fileDirForDatabase) throws ExportServiceException {
+
+        try {
+            JournalAbbreviationRepository abbreviationRepository = Injector.instantiateModelOrService(JournalAbbreviationRepository.class);
+            System.out.println(Localization.lang("Exporting %0", outputFile.toAbsolutePath().toString()));
+            exporter.export(databaseContext, outputFile, entries, fileDirForDatabase, abbreviationRepository);
+        } catch (IOException | SaveException | ParserConfigurationException | TransformerException ex) {
+            throw new ExportServiceException("Failed to export file.",
+                    Localization.lang("Failed to export file."),
+                    ex, CommandLine.ExitCode.SOFTWARE);
+        }
Evidence
exportParserResultToFile(...) conditionally prints based on porcelain, but
tryExportWithExporter(...) prints regardless and is called by exportParserResultToFile(...),
forcing output even in porcelain mode and duplicating messages otherwise.

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[173-193]
jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[195-210]
jabkit/src/main/java/org/jabref/toolkit/commands/Convert.java[53-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ExportService.tryExportWithExporter(...)` prints to stdout unconditionally. This causes:
- Duplicate "Exporting" messages (one in `exportParserResultToFile`, one in `tryExportWithExporter`).
- `--porcelain` output to be polluted with status text (e.g., `Convert` passes `sharedOptions.porcelain`, but it is not respected).
## Issue Context
Porcelain is documented as "script-friendly output" and should avoid non-data status lines.
## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[173-211]
- jabkit/src/main/java/org/jabref/toolkit/commands/Convert.java[53-64]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Missing requirement for citation output 📘 Rule violation ⚙ Maintainability
Description
This PR adds a new user-facing feature (file output options for
get-cited-works/get-citing-works) without adding an OpenFastTrace requirement entry. This breaks
the project’s requirements traceability process for new features.
Code

jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[R37-48]

+    @CommandLine.Option(
+            names = "--output",
+            description = "Write output to this file (e.g. --output=out.bib)"
+    )
+    private Path outputFile;
+
+    @CommandLine.Option(
+            names = "--output-format",
+            description = "Output format (e.g. bibtex)",
+            defaultValue = "bibtex"
+    )
+    private String outputFormat;
Evidence
Rule 23 requires a requirement entry for new features. The diff adds new file output options to
citation commands, which is a user-facing CLI feature change.

AGENTS.md: OpenFastTrace Requirements: Add Requirement Entries for New Features/Significant Fixes and Follow Required Format: AGENTS.md: OpenFastTrace Requirements: Add Requirement Entries for New Features/Significant Fixes and Follow Required Format: AGENTS.md: OpenFastTrace Requirements: Add Requirement Entries for New Features/Significant Fixes and Follow Required Format: AGENTS.md: OpenFastTrace Requirements: Add Requirement Entries for New Features/Significant Fixes and Follow Required Format
jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[37-48]
jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[37-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new CLI feature was added, but no OpenFastTrace requirement entry was introduced.
## Issue Context
Compliance requires new features/significant fixes to be reflected in `docs/requirements/<area>.md` using the required OpenFastTrace heading+identifier format.
## Fix Focus Areas
- docs/requirements/cli.md[1-120]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[37-48]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[37-48]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. BibTeX exporter ignores entries ✓ Resolved 🐞 Bug ≡ Correctness
Description
The custom bibtexExporter ignores the entries parameter and always saves the full
BibDatabaseContext, so any call site that exports a subset via
exportBibDatabaseContextToFile(context, subset, ...) can accidentally export more entries than
requested.
Code

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[R61-67]

+    private Exporter createBibtexExporter() {
+        return new Exporter("bibtex", "BibTex", StandardFileType.BIBTEX_DB) {
+            @Override
+            public void export(BibDatabaseContext databaseContext, Path file, List<BibEntry> entries) throws IOException {
+                internalSaveDatabaseContext(databaseContext, file);
+            }
+        };
Evidence
Exporter.export(...) is defined to receive the entries that should be exported, but the BibTeX
exporter implementation drops that parameter and writes the whole context instead, which can be
larger than the passed subset list.

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[61-67]
jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[163-171]
jablib/src/main/java/org/jabref/logic/exporter/Exporter.java[53-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The custom BibTeX exporter implementation ignores the `entries` list passed to `Exporter.export(...)`. This violates the exporter contract (export the provided subset) and can lead to exporting unintended entries when a `BibDatabaseContext` contains more entries than the subset list.
## Issue Context
The Exporter API explicitly passes `List<BibEntry> entries` to allow subset exporting.
## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[61-67]
- jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java[151-171]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add file output options to citation commands with service layer refactoring

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add file output and format options to get-cited-works and get-citing-works commands
• Centralize import/export functionality into dedicated service layer (ImportService,
  ExportService)
• Introduce CliException and CliExceptionHandler to reduce try-catch boilerplate
• Create CitationFetcherFactory service to encapsulate citation fetcher creation
• Refactor commands to use services instead of static methods for better testability
Diagram
flowchart LR
  A["Commands<br/>get-cited-works<br/>get-citing-works"] -->|use| B["ExportService"]
  A -->|use| C["CitationFetcherFactory"]
  D["Other Commands<br/>Convert, Search, etc."] -->|use| B
  D -->|use| E["ImportService"]
  B -->|throws| F["ExportServiceException"]
  E -->|throws| G["ImportServiceException"]
  F -->|extends| H["CliException"]
  G -->|extends| H
  H -->|handled by| I["CliExceptionHandler"]

Loading

Grey Divider

File Changes

1. jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java ✨ Enhancement +238/-0

New service layer for export functionality

jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java


2. jabkit/src/main/java/org/jabref/toolkit/service/ImportService.java ✨ Enhancement +131/-0

New service layer for import functionality

jabkit/src/main/java/org/jabref/toolkit/service/ImportService.java


3. jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java ✨ Enhancement +36/-0

Factory for creating citation fetcher instances

jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java


View more (37)
4. jabkit/src/main/java/org/jabref/toolkit/exception/CliException.java Error handling +27/-0

Base exception class for CLI errors

jabkit/src/main/java/org/jabref/toolkit/exception/CliException.java


5. jabkit/src/main/java/org/jabref/toolkit/exception/ExportServiceException.java Error handling +15/-0

Exception for export service errors

jabkit/src/main/java/org/jabref/toolkit/exception/ExportServiceException.java


6. jabkit/src/main/java/org/jabref/toolkit/exception/ImportServiceException.java Error handling +15/-0

Exception for import service errors

jabkit/src/main/java/org/jabref/toolkit/exception/ImportServiceException.java


7. jabkit/src/main/java/org/jabref/toolkit/exception/CliExceptionHandler.java Error handling +44/-0

Centralized exception handler for CLI commands

jabkit/src/main/java/org/jabref/toolkit/exception/CliExceptionHandler.java


8. jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java ✨ Enhancement +40/-32

Add output file and format options to command

jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java


9. jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java ✨ Enhancement +40/-32

Add output file and format options to command

jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java


10. jabkit/src/main/java/org/jabref/toolkit/commands/Convert.java Refactoring +12/-74

Refactor to use ImportService and ExportService

jabkit/src/main/java/org/jabref/toolkit/commands/Convert.java


11. jabkit/src/main/java/org/jabref/toolkit/commands/Search.java Refactoring +11/-55

Refactor to use ImportService and ExportService

jabkit/src/main/java/org/jabref/toolkit/commands/Search.java


12. jabkit/src/main/java/org/jabref/toolkit/commands/GenerateBibFromAux.java Refactoring +15/-21

Refactor to use ImportService and ExportService

jabkit/src/main/java/org/jabref/toolkit/commands/GenerateBibFromAux.java


13. jabkit/src/main/java/org/jabref/toolkit/commands/GenerateCitationKeys.java Refactoring +12/-22

Refactor to use ImportService and ExportService

jabkit/src/main/java/org/jabref/toolkit/commands/GenerateCitationKeys.java


14. jabkit/src/main/java/org/jabref/toolkit/commands/Pseudonymize.java Refactoring +9/-22

Refactor to use ImportService and ExportService

jabkit/src/main/java/org/jabref/toolkit/commands/Pseudonymize.java


15. jabkit/src/main/java/org/jabref/toolkit/commands/CheckConsistency.java Refactoring +6/-7

Refactor to use ImportService for file loading

jabkit/src/main/java/org/jabref/toolkit/commands/CheckConsistency.java


16. jabkit/src/main/java/org/jabref/toolkit/commands/CheckIntegrity.java Refactoring +7/-7

Refactor to use ImportService for file loading

jabkit/src/main/java/org/jabref/toolkit/commands/CheckIntegrity.java


17. jabkit/src/main/java/org/jabref/toolkit/commands/Check.java Refactoring +25/-2

Add exception handling for consistency and integrity checks

jabkit/src/main/java/org/jabref/toolkit/commands/Check.java


18. jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java Refactoring +5/-13

Refactor to use ImportService for file loading

jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java


19. jabkit/src/main/java/org/jabref/toolkit/commands/DoiToBibtex.java Refactoring +5/-2

Refactor to use ExportService for output

jabkit/src/main/java/org/jabref/toolkit/commands/DoiToBibtex.java


20. jabkit/src/main/java/org/jabref/toolkit/commands/Fetch.java Refactoring +15/-15

Refactor to use ExportService and CliException

jabkit/src/main/java/org/jabref/toolkit/commands/Fetch.java


21. jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java Refactoring +6/-2

Register CliExceptionHandler and use services

jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java


22. jabkit/src/main/java/org/jabref/toolkit/commands/AbstractJabKitTest.java 🧪 Tests +0/-0

Extract output capturing into CapturingCommandLine

jabkit/src/main/java/org/jabref/toolkit/commands/AbstractJabKitTest.java


23. jabkit/src/test/java/org/jabref/toolkit/util/CapturingCommandLine.java 🧪 Tests +61/-0

New utility class for capturing command output in tests

jabkit/src/test/java/org/jabref/toolkit/util/CapturingCommandLine.java


24. jabkit/src/test/java/org/jabref/toolkit/util/CommandFactory.java 🧪 Tests +36/-0

Factory for injecting mocked commands in tests

jabkit/src/test/java/org/jabref/toolkit/util/CommandFactory.java


25. jabkit/src/test/java/org/jabref/toolkit/commands/GetCitedWorksTest.java 🧪 Tests +130/-0

New test class for get-cited-works command

jabkit/src/test/java/org/jabref/toolkit/commands/GetCitedWorksTest.java


26. jabkit/src/test/java/org/jabref/toolkit/commands/GetCitingWorksTest.java 🧪 Tests +130/-0

New test class for get-citing-works command

jabkit/src/test/java/org/jabref/toolkit/commands/GetCitingWorksTest.java


27. jabkit/src/test/java/org/jabref/toolkit/service/ExportServiceTest.java 🧪 Tests +88/-0

Test suite for ExportService with multiple formats

jabkit/src/test/java/org/jabref/toolkit/service/ExportServiceTest.java


28. jabkit/src/test/java/org/jabref/toolkit/service/ImportServiceTest.java 🧪 Tests +35/-0

Test suite for ImportService functionality

jabkit/src/test/java/org/jabref/toolkit/service/ImportServiceTest.java


29. jabkit/src/test/java/org/jabref/toolkit/commands/ConvertTest.java 🧪 Tests +1/-75

Remove duplicate parameterized export format tests

jabkit/src/test/java/org/jabref/toolkit/commands/ConvertTest.java


30. jabkit/src/test/java/org/jabref/toolkit/commands/JabKitTest.java 🧪 Tests +10/-11

Update tests to use CapturingCommandLine

jabkit/src/test/java/org/jabref/toolkit/commands/JabKitTest.java


31. jabkit/src/test/java/org/jabref/toolkit/commands/PseudonymizeTest.java Refactoring +7/-7

Update to use NIO Files API instead of Guava

jabkit/src/test/java/org/jabref/toolkit/commands/PseudonymizeTest.java


32. jabkit/src/test/resources/org/jabref/toolkit/service/origin.bib 🧪 Tests +24/-0

Test resource file for service tests

jabkit/src/test/resources/org/jabref/toolkit/service/origin.bib


33. docs/decisions/0062-separate-jabkit-commands-and-services.md 📝 Documentation +37/-0

ADR for command and service separation pattern

docs/decisions/0062-separate-jabkit-commands-and-services.md


34. docs/decisions/0063-use-picocli-exceptionhandler.md 📝 Documentation +38/-0

ADR for picocli exception handler usage

docs/decisions/0063-use-picocli-exceptionhandler.md


35. .jbang/JabKitLauncher.java ⚙️ Configuration changes +9/-0

Add new service and exception sources to JBang config

.jbang/JabKitLauncher.java


36. CHANGELOG.md 📝 Documentation +1/-0

Document new citation command output features

CHANGELOG.md


37. jabkit/build.gradle.kts Additional files +0/-1

...

jabkit/build.gradle.kts


38. jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java Additional files +0/-217

...

jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java


39. jabkit/src/test/java/org/jabref/toolkit/commands/AbstractJabKitTest.java Additional files +9/-52

...

jabkit/src/test/java/org/jabref/toolkit/commands/AbstractJabKitTest.java


40. jablib/src/main/resources/l10n/JabRef_en.properties Additional files +0/-1

...

jablib/src/main/resources/l10n/JabRef_en.properties


Grey Divider

Qodo Logo

Comment thread jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java
Comment thread jabkit/src/main/java/org/jabref/toolkit/commands/Check.java
Comment on lines +24 to +35
public CitationFetcher getCitationFetcher(CitationFetcherType citationFetcherType) {
ChatModel chatModel = ChatModelFactory.create(cliPreferences.getAiPreferences());
return CitationFetcherType.getCitationFetcher(
citationFetcherType,
cliPreferences.getImporterPreferences(),
cliPreferences.getImportFormatPreferences(),
cliPreferences.getCitationKeyPatternPreferences(),
cliPreferences.getGrobidPreferences(),
cliPreferences.getAiPreferences(),
chatModel
);
}

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.

Action required

4. Chatmodel resource leak 🐞 Bug ☼ Reliability

CitationFetcherFactory.getCitationFetcher(...) creates a ChatModel but never closes it; since
ChatModel is AutoCloseable, repeated use can leak resources such as threads or HTTP connections.
Agent Prompt
## Issue description
`CitationFetcherFactory.getCitationFetcher(...)` constructs a `ChatModel` but does not close it. `ChatModel` implements `AutoCloseable`, so this is a resource leak.

## Issue Context
Previously, commands used try-with-resources around `ChatModel`. After refactoring, the lifecycle is lost.

A robust fix is to return a handle/wrapper that implements `AutoCloseable` and closes the `ChatModel` (and any other resources) when done, and to use it with try-with-resources in `GetCitedWorks`/`GetCitingWorks`.

## Fix Focus Areas
- jabkit/src/main/java/org/jabref/toolkit/service/CitationFetcherFactory.java[24-35]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java[60-80]
- jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java[60-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread jabkit/src/main/java/org/jabref/toolkit/service/ExportService.java
@JohnnyGoodNews

Copy link
Copy Markdown
Author

Thank you both for your feedback and time.

I addressed the mentioned issues. Should I mark those points as 'resolve conversation'? (Or is that for the reviewers)

@subhramit

Copy link
Copy Markdown
Member

Should I mark those points as 'resolve conversation'? (Or is that for the reviewers)

Yeah you can leave them as-is. If it starts getting too cluttered we'll resolve them ourselves. We keep them open until them to track changes over subsequent review iterations.

Comment thread CHANGELOG.md Outdated
- The `jabkit check` command now runs both the consistency and integrity checks when given an input file without a subcommand (e.g. `jabkit check references.bib`). [#15759](https://github.com/JabRef/jabref/pull/15759)
- We added OCR feature using OCRmyPDF to extract text from scanned PDFs and create searchable PDFs including the extracted text. [#15712](https://github.com/JabRef/jabref/pull/15712)
- We Added generic CSV export filter that exports all standard BibTeX fields [#15711](https://github.com/JabRef/jabref/issues/15711)
- We added support to the JabKit commands `get-cited-works` and `get-citing-works` to output to files in various export formats

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.

Add link to PR.


@Override
public Integer call() {
public Integer call() throws ImportServiceException, ExportServiceException {

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.

@calixtus This is one "pattern" to investigate. Even thow the handling of "expected" excpetional cases is now non-local, I tend to like it --> more Java'ish control flow (exceptions --> BibTeX file not parseable is rather an exception than normal flow, isn't it?)

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.

We discussed this and came to the following conclusion:

  1. Effective Java (J. Bloch) states, that Exceptions should only be thrown for exceptional states. We try to follow the design principles of EJ.
    Demonstration by a file not found example:
  2. An unexpected exception may happen if either the path given by the user was not tested on entry (robustness) or was removed during operation (bc eg. cloud connection broke midway).
  3. If the internal api recieves information, it should expect the information to be valid. File errors are then unexpected an throwing an exception is correct, if the data given was validated before.

@koppor is this correctly summarized?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this correctly summarized?

Yeah - and the consequence is that before the existance and access check should be made? -- is this "nice" to do using PicoCLI?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Just to chime in: PicoCLI support JSR-380 BeanValidation which is nice and reusable (and de-facto industry standard). (https://picocli.info/#_validation) The docs suggest throwing ParameterException in reaction to invalid user input.

Treating the Command classes as "driving/inbound ports" like RestControllers where Validation/Cleanup happens before the actual work is delegated to service/use-case/interaction logic seems appropriate to me.

@JohnnyGoodNews

JohnnyGoodNews commented Jun 7, 2026

Copy link
Copy Markdown
Author

Open from Qodo review:

  • 1. the new Picocli options (--output, --output-format) emit hardcoded, non-localized English user-facing text
    • There should be a solution CommandLine.Option#descriptionKey. However I am not sure how to bridge that to the JabRef Localization/ResourceBundle class (and a separate issue might be appropriate).
    • Option/Parameter Localization of help description: use descriptionKey instead of description and in JabKitLauncher set commandLine.setResourceBundle(Localization.getMessages());
  • 6. Missing requirement for citation output
    • To me the feature / behavior change seems too small for a dedicated requirement (or a requirement should be formulated for all Commands that support file output)
  • 4. Chatmodel resource leak / Closable not closed
    • This is a bit difficult for me to tackle and test. Is there maybe a ShutdownHook that can be used?
    • Maybe an option is decoupling ChatModel and HttpClient/Connection and use a ConnectionPool?

@koppor

koppor commented Jun 8, 2026

Copy link
Copy Markdown
Member

. Chatmodel resource leak / Closable not closed

I would be very "naive" here:

  1. JVM shuts down and will "surely" also close this
  2. Old code missed lifecycle management, too. Thus, not on you.

Missing requirement for citation output

Very OK - no req needed.

Comment on lines +18 to +22
## Considered Options

* Use specialized Utility classes for importing/exporting/fetching with static methods
* Introduce shared JabKit services to separate Commands and logic
* Keep import/export logic and fetcher creation inside each command

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.

Nitpick: Normally we list pros and cons for every possible option, but is no blocker

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.

Kind of the "slim" MADR format for "easier" ADRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: JabKit [cli] dev: code-quality Issues related to code or architecture decisions first contrib

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants