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