diff --git a/.github/workflows/tests-code.yml b/.github/workflows/tests-code.yml
index 77294331696..7fce6d27f6b 100644
--- a/.github/workflows/tests-code.yml
+++ b/.github/workflows/tests-code.yml
@@ -469,6 +469,7 @@ jobs:
run: |
echo "cache_key=jbang-$(date +%F)" >> $GITHUB_OUTPUT
- name: Use cache
+ if: steps.changed-jablib-files.outputs.any_changed != 'true'
uses: actions/cache@v4
with:
path: ~/.jbang
@@ -522,6 +523,7 @@ jobs:
with:
files: |
.jbang/*.java
+ jabkit/src/main/java/**/*.java
jablib/src/main/java/**/*.java
jablib-examples/**/*.java
files_ignore: |
@@ -553,7 +555,16 @@ jobs:
# We modify the JBang scripts directly to avoid issues with relative paths
for f in ${{ steps.changed-jablib-files.outputs.all_changed_files }}; do
case "$f" in
- jablib-examples/*) continue ;; # skip scripts
+ jablib-examples/*)
+ # skip scripts
+ continue
+ ;;
+ jabkit/*)
+ # only JabKit needs its modified sources
+ if [ "${{ matrix.script }}" != ".jbang/JabKitLauncher.java" ]; then
+ continue
+ fi
+ ;;
esac
echo "//SOURCES ../$f" >> "${{ matrix.script }}"
done
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb9108a09a2..59b97dd56c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,9 +13,11 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added "IEEE" as another option for parsing plain text citations. [#14233](github.com/JabRef/jabref/pull/14233)
- We added automatic date-based groups that create year/month/day subgroups from an entry’s date fields. [#10822](https://github.com/JabRef/jabref/issues/10822)
+- We added `doi-to-bibtex` to `JabKit`. [#14244](https://github.com/JabRef/jabref/pull/14244)
### Changed
+- `JabKit`: `--porcelain` does not output any logs to the console anymore. [#14244](https://github.com/JabRef/jabref/pull/14244)
- Ctrl + Shift + L now opens the terminal in the active library directory. [#14130](https://github.com/JabRef/jabref/issues/14130)
### Fixed
diff --git a/jabkit/build.gradle.kts b/jabkit/build.gradle.kts
index 7c5c1d679ec..c97012295d4 100644
--- a/jabkit/build.gradle.kts
+++ b/jabkit/build.gradle.kts
@@ -68,6 +68,9 @@ application {
// Also passed to launcher by java-module-packaging plugin
applicationDefaultJvmArgs = listOf(
+ // JEP 158: Disable all java util logging
+ "-Xlog:disable",
+
// Enable JEP 450: Compact Object Headers
"-XX:+UnlockExperimentalVMOptions", "-XX:+UseCompactObjectHeaders",
diff --git a/jabkit/src/main/java/org/jabref/JabKit.java b/jabkit/src/main/java/org/jabref/JabKit.java
index 04f917af1d5..2265bb17fc2 100644
--- a/jabkit/src/main/java/org/jabref/JabKit.java
+++ b/jabkit/src/main/java/org/jabref/JabKit.java
@@ -7,7 +7,6 @@
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -58,6 +57,8 @@ public class JabKit {
private static final String JABKIT_BRAND = "JabKit - command line toolkit for JabRef";
+ /// Note: To test with gradle, use jabkit -> Tasks -> application -> run
+ /// Use `--args="..."` as parameters to "Run"
public static void main(String[] args) {
initLogging(args);
@@ -143,11 +144,10 @@ public static void initLogging(String[] args) {
// We must configure logging as soon as possible, which is why we cannot wait for the usual
// argument parsing workflow to parse logging options e.g. --debug or --porcelain
+ boolean isPorcelain = Arrays.stream(args).anyMatch("--porcelain"::equalsIgnoreCase);
Level logLevel;
if (Arrays.stream(args).anyMatch("--debug"::equalsIgnoreCase)) {
logLevel = Level.DEBUG;
- } else if (Arrays.stream(args).anyMatch("--porcelain"::equalsIgnoreCase)) {
- logLevel = Level.ERROR;
} else {
logLevel = Level.INFO;
}
@@ -163,18 +163,23 @@ public static void initLogging(String[] args) {
return;
}
+ String fileWriterName;
+ if (isPorcelain) {
+ fileWriterName = "writer";
+ } else {
+ fileWriterName = "writerFile";
+ }
+
// The "Shared File Writer" is explained at
// https://tinylog.org/v2/configuration/#shared-file-writer
- Map configuration = Map.of(
- "level", logLevel.name().toLowerCase(),
- "writerFile", "rolling file",
- "writerFile.logLevel", logLevel == Level.DEBUG ? "debug" : "info",
- // We need to manually join the path, because ".resolve" does not work on Windows, because ":" is not allowed in file names on Windows
- "writerFile.file", directory + File.separator + "log_{date:yyyy-MM-dd_HH-mm-ss}.txt",
- "writerFile.charset", "UTF-8",
- "writerFile.policies", "startup",
- "writerFile.backups", "30");
- configuration.forEach(Configuration::set);
+ Configuration.set("level", logLevel.name().toLowerCase());
+ Configuration.set(fileWriterName, "rolling file");
+ Configuration.set("%s.logLevel".formatted(fileWriterName), logLevel == Level.DEBUG ? "debug" : "info");
+ // We need to manually join the path, because ".resolve" does not work on Windows, because ":" is not allowed in file names on Windows
+ Configuration.set("%s.file".formatted(fileWriterName), directory + File.separator + "log_{date:yyyy-MM-dd_HH-mm-ss}.txt");
+ Configuration.set("%s.charset".formatted(fileWriterName), "UTF-8");
+ Configuration.set("%s.policies".formatted(fileWriterName), "startup");
+ Configuration.set("%s.backups".formatted(fileWriterName), "30");
LOGGER = LoggerFactory.getLogger(JabKit.class);
}
diff --git a/jabkit/src/main/java/org/jabref/cli/ArgumentProcessor.java b/jabkit/src/main/java/org/jabref/cli/ArgumentProcessor.java
index 6c2f00754c6..e14dc409f92 100644
--- a/jabkit/src/main/java/org/jabref/cli/ArgumentProcessor.java
+++ b/jabkit/src/main/java/org/jabref/cli/ArgumentProcessor.java
@@ -43,6 +43,7 @@
CheckConsistency.class,
CheckIntegrity.class,
Convert.class,
+ DoiToBibtex.class,
Fetch.class,
GenerateBibFromAux.class,
GenerateCitationKeys.class,
diff --git a/jabkit/src/main/java/org/jabref/cli/DoiToBibtex.java b/jabkit/src/main/java/org/jabref/cli/DoiToBibtex.java
new file mode 100644
index 00000000000..8340e5d7232
--- /dev/null
+++ b/jabkit/src/main/java/org/jabref/cli/DoiToBibtex.java
@@ -0,0 +1,87 @@
+package org.jabref.cli;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+
+import org.jabref.logic.exporter.BibDatabaseWriter;
+import org.jabref.logic.importer.FetcherException;
+import org.jabref.logic.importer.fetcher.CrossRef;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.model.database.BibDatabase;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.identifier.DOI;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Parameters;
+
+@Command(name = "doi-to-bibtex", description = "Converts a DOI to BibTeX")
+public class DoiToBibtex implements Callable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DoiToBibtex.class);
+
+ @CommandLine.ParentCommand
+ private ArgumentProcessor argumentProcessor;
+
+ @CommandLine.Mixin
+ private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions();
+
+ @Parameters(paramLabel = "DOI", description = "one or more DOIs to fetch", arity = "1..*")
+ private String[] dois;
+
+ @Override
+ public Integer call() {
+ CrossRef fetcher = new CrossRef();
+ List entries = new ArrayList<>(dois.length);
+
+ for (String doiString : dois) {
+ Optional doiParsed = DOI.parse(doiString);
+ if (doiParsed.isEmpty()) {
+ LOGGER.warn("Skipped DOI {}, because it is not a valid DOI string", doiString);
+ System.out.println(Localization.lang("DOI %0 is invalid", doiString));
+ System.err.println();
+ continue;
+ }
+ Optional entry;
+ try {
+ entry = fetcher.performSearchById(doiParsed.get().asString());
+ } catch (FetcherException e) {
+ LOGGER.error("Could not fetch BibTeX based on DOI", e);
+ System.err.print(Localization.lang("No data was found for the identifier"));
+ System.err.println(" - " + doiString);
+ System.err.println(e.getLocalizedMessage());
+ System.err.println();
+ continue;
+ }
+
+ if (entry.isEmpty()) {
+ LOGGER.error("Could not fetch BibTeX based on DOI - entry is empty");
+ System.err.print(Localization.lang("No data was found for the identifier"));
+ System.err.println(" - " + doiString);
+ System.err.println();
+ continue;
+ }
+
+ entries.add(entry.get());
+ }
+
+ try (OutputStreamWriter writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8)) {
+ BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(entries));
+ BibDatabaseWriter bibWriter = new BibDatabaseWriter(writer, context, argumentProcessor.cliPreferences);
+ bibWriter.writeDatabase(context);
+ } catch (IOException e) {
+ LOGGER.error("Could not write BibTeX", e);
+ System.err.println(Localization.lang("Unable to write to %0.", "stdout"));
+ return 1;
+ }
+ return 0;
+ }
+}
diff --git a/jablib-examples/doi_to_bibtex.java b/jablib-examples/doi_to_bibtex.java
new file mode 100644
index 00000000000..ec82e3551d9
--- /dev/null
+++ b/jablib-examples/doi_to_bibtex.java
@@ -0,0 +1,34 @@
+///usr/bin/env jbang "$0" "$@" ; exit $?
+
+import org.jabref.logic.exporter.BibWriter;
+import org.jabref.logic.exporter.BibDatabaseWriter;
+import org.jabref.logic.importer.fetcher.CrossRef;
+import org.jabref.logic.preferences.JabRefCliPreferences;
+import org.jabref.model.database.BibDatabase;
+import org.jabref.model.database.BibDatabaseContext;
+
+import org.tinylog.Logger;
+
+//DESCRIPTION Converts a DOI to BibTeX
+
+//JAVA 25+
+//RUNTIME_OPTIONS --enable-native-access=ALL-UNNAMED
+//FILES tinylog.properties=tinylog.properties
+
+//DEPS org.jabref:jablib:6.0-SNAPSHOT
+//REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/,s01oss=https://s01.oss.sonatype.org/content/repositories/snapshots/,oss=https://oss.sonatype.org/content/repositories,jitpack=https://jitpack.io,oss2=https://oss.sonatype.org/content/groups/public,ossrh=https://oss.sonatype.org/content/repositories/snapshots,raw=https://raw.githubusercontent.com/JabRef/jabref/refs/heads/main/jablib/lib/
+
+void main() throws Exception {
+ var preferences = JabRefCliPreferences.getInstance();
+
+ // All `IdParserFetcher` can do. In JabRef, there is currently only one implemented
+
+ var fetcher = new CrossRef();
+ var entry = fetcher.performSearchById("10.47397/tb/44-3/tb138kopp-jabref").get(); // will throw an exception if not found
+
+ try (var writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8)) {
+ var context = new BibDatabaseContext(new BibDatabase(List.of(entry)));
+ var bibWriter = new BibDatabaseWriter(writer, context, preferences);
+ bibWriter.writeDatabase(context);
+ }
+}
diff --git a/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java
index ae18dfb1089..61449bb5562 100644
--- a/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java
+++ b/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java
@@ -32,7 +32,7 @@
import org.jabref.logic.cleanup.FieldFormatterCleanups;
import org.jabref.logic.cleanup.NormalizeWhitespacesCleanup;
import org.jabref.logic.formatter.bibtexfields.TrimWhitespaceFormatter;
-import org.jabref.logic.preferences.JabRefCliPreferences;
+import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.FieldChange;
import org.jabref.model.database.BibDatabase;
@@ -95,7 +95,7 @@ public BibDatabaseWriter(@NonNull BibWriter bibWriter,
/// @param preferences - used to read all the preferences
public BibDatabaseWriter(@NonNull Writer writer,
@NonNull BibDatabaseContext bibDatabaseContext,
- @NonNull JabRefCliPreferences preferences) {
+ @NonNull CliPreferences preferences) {
this(new BibWriter(writer, bibDatabaseContext.getDatabase().getNewLineSeparator()),
preferences.getSelfContainedExportConfiguration(),
preferences.getFieldPreferences(),
diff --git a/jablib/src/main/java/org/jabref/logic/importer/plaincitation/PlainCitationParser.java b/jablib/src/main/java/org/jabref/logic/importer/plaincitation/PlainCitationParser.java
index f2417ba3d70..b6a5c7a8fbc 100644
--- a/jablib/src/main/java/org/jabref/logic/importer/plaincitation/PlainCitationParser.java
+++ b/jablib/src/main/java/org/jabref/logic/importer/plaincitation/PlainCitationParser.java
@@ -2,7 +2,6 @@
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
import org.jabref.logic.importer.FetcherException;
import org.jabref.model.entry.BibEntry;
@@ -19,7 +18,7 @@ default List parseMultiplePlainCitations(String text) throws FetcherEx
return CitationSplitter.splitCitations(text)
.map(Unchecked.function(this::parsePlainCitation))
.flatMap(Optional::stream)
- .collect(Collectors.toList());
+ .toList();
} catch (UncheckedException e) {
throw (FetcherException) e.getCause();
}
diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties
index 14d987aad0c..6a63f88c2b7 100644
--- a/jablib/src/main/resources/l10n/JabRef_en.properties
+++ b/jablib/src/main/resources/l10n/JabRef_en.properties
@@ -1450,6 +1450,7 @@ No\ problems\ found.=No problems found.
Print\ entry\ preview=Print entry preview
Invalid\ DOI\:\ '%0'.=Invalid DOI: '%0'.
+DOI\ %0\ is\ invalid=DOI %0 is invalid
Same\ DOI\ used\ in\ multiple\ entries=Same DOI used in multiple entries
should\ start\ with\ a\ name=should start with a name
should\ end\ with\ a\ name=should end with a name
@@ -1974,7 +1975,6 @@ Update\ with\ bibliographic\ information\ from\ the\ web=Update with bibliograph
Could\ not\ find\ any\ bibliographic\ information.=Could not find any bibliographic information.
Citation\ key\ deviates\ from\ generated\ key=Citation key deviates from generated key
-DOI\ %0\ is\ invalid=DOI %0 is invalid
Select\ all\ customized\ types\ to\ be\ stored\ in\ local\ preferences\:=Select all customized types to be stored in local preferences\:
Different\ customization,\ current\ settings\ will\ be\ overwritten=Different customization, current settings will be overwritten