From a1b6d3808d2994241ee6e06491ff2f714ae19c74 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Thu, 21 Mar 2024 23:37:57 +0100 Subject: [PATCH] Improve CFF import/export and craft a round-trip test (#10995) * issue #10993 - feat: added ability to parse preferred-citation field to CffImporter * issue #10993 - feat: added all fields of JabRef/CITATION.cff to CffImporter * issue #10993 - feat: rewrote CffExporter to parse Software, Dataset types and authors names correctly * issue #10993 - feat: added keywords and unknown fields support * issue #10993 - feat: added round-trip test * issue #10993 - doc: updated CHANGELOG.md * Convert RemoveBracesFormatterTest to @ParameterizedTest (#11033) * Convert to @ParameterizedTest * Convert to csvsource --------- Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> * Importing of BibDesk Groups and Linked Files (#10968) * Add test to check parsing of BibDesk Static Groups * Add test to check parsing of BibDesk Static Groups * Change isExpanded attribute to false in expected groups * remove extra blank line * Add tests to check parsing of BibDesk Smart and mixed groups * Add parsing of BibDesk Files * Attempts at plist * Now parses bdsk-file and shows it as a file in JabRef * Add test for parsing a bdsk-file field * Fix formatting * Add dd-plist library to documentation --------- Co-authored-by: Tian0602 <646432316@qq.com> * Add creation of static JabRef group from a BibDesk file * Creates an empty ExplicitGroup from BibDesk comment * Adds citations to new groups modifies group creations to support multiple groups in the same BibDeskFile * Fix requested changes Refactor imports since they did not match with main Add safety check in addBibDeskGroupEntriesToJabRefGroups --------- Co-authored-by: Filippa Nilsson * Refactor newline to match main branch Co-authored-by: Filippa Nilsson * Add changes to CHANGELOG.md * Reformat indentation to match previous * Revert external libraries Adjust groups serializing * checkstyle and optional magic * fix * fix tests * fix * fix dangling do * better group tree metadata setting * merge group trees, prevent duplicate group assignment in entry Add new BibDesk group Fix IOB for change listeing * fix tests, and extract constant * return early * fixtest and checkstyle --------- Co-authored-by: Anna Maartensson <120831475+annamaartensson@users.noreply.github.com> Co-authored-by: Tian0602 <646432316@qq.com> Co-authored-by: LottaJohnsson <35195355+LottaJohnsson@users.noreply.github.com> Co-authored-by: Filippa Nilsson Co-authored-by: Filippa Nilsson <75281470+filippanilsson@users.noreply.github.com> Co-authored-by: Oliver Kopp Co-authored-by: Siedlerchr * Speed up failure reporting (#11030) * Fixes Zotero file handling for absolute paths (#11038) * Fixes Zotero file handling for absolute paths Fixes #10959 * checkstyle mimiimm * fix changelog * cannot fix * Change copy-paste function to handle string constants (follow up PR) (#11037) * [Copy] Include string constants in copy (#11) Signed-off-by: Anders Blomqvist * [Copy] New method for serializing string constants (#12) Signed-off-by: Anders Blomqvist * Add a sanity check for null for clipboard content Currenlty, the clipboard content can be null since the database does not seem to be updating. This is a sanity check to prevent the program from adding null to the clipboard. Link to DD2480-Group1/jabref#13 * [Fix] Add parsed serilization when save settings When loading from existing files or libraries, the parser will set the serilization of the string constant to the correct value. However, when editing via the GUI, the serilization was not set and a new string constant list will be created without the serilization. This result in the serilization being null and when copying with the clipboard. Link to DD2480-Group1/jabref#13 * feat: import string constants when pasting #9 Add functionality to import string constants in the paste function Should add functionality to handle colliding string constants. Should also check that the constants are valid using the ConstantsItemModel class. * feat: Add string constant validity checker and dialog messages #9 Check that a pasted string constant is valid using the ConstantsItemModel class. Add diagnostic messages notifying users when adding a string constant fails while pasting. * [Copy] Copy referenced constant strings to clipboard (#16) * feat: Add parsed serialized string when cloning * feat: Add sanity check for null in ClipBoardManager * closes #15 * feat: new unit tests Add 4 new unit tests, testing the new features added for issue-10872. Specifically the tests are for the `storeSettings` method in the ConstantsPropertiesViewModel.java, and `setContent` in the ClipBaordManager.java. Closes #6 * Update CHANGELOG with copy and paste function * Fix Checkstyle failing by reformat the code * Fix OpenRewrite failing by running rewriteRun * Refactor by extract methods in setContent * collet failures * changelog and use os.newline * checkstyle * use real bibentrytypes manager * Fix CHANGELOG.md * Swap if branches * Code cleanup * Use List for getUsedStringValues * Fix submodule * Collection is better * Fix csl-styles * Remove empty line * Group BibTeX string l10n together --------- Signed-off-by: Anders Blomqvist Co-authored-by: Anders Blomqvist Co-authored-by: ZOU Hetai <33616271+JXNCTED@users.noreply.github.com> Co-authored-by: Hannes Stig Co-authored-by: Elliot Co-authored-by: Oliver Kopp * Bump gittools/actions from 0.13.4 to 1.1.1 (#11039) Bumps [gittools/actions](https://github.com/gittools/actions) from 0.13.4 to 1.1.1. - [Release notes](https://github.com/gittools/actions/releases) - [Commits](https://github.com/gittools/actions/compare/v0.13.4...v1.1.1) --- updated-dependencies: - dependency-name: gittools/actions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump com.googlecode.plist:dd-plist from 1.23 to 1.28 (#11040) Bumps [com.googlecode.plist:dd-plist](https://github.com/3breadt/dd-plist) from 1.23 to 1.28. - [Release notes](https://github.com/3breadt/dd-plist/releases) - [Commits](https://github.com/3breadt/dd-plist/compare/dd-plist-1.23...v1.28.0) --- updated-dependencies: - dependency-name: com.googlecode.plist:dd-plist dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump org.apache.pdfbox:xmpbox from 3.0.1 to 3.0.2 (#11041) Bumps org.apache.pdfbox:xmpbox from 3.0.1 to 3.0.2. --- updated-dependencies: - dependency-name: org.apache.pdfbox:xmpbox dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump com.dlsc.gemsfx:gemsfx from 2.2.0 to 2.4.0 (#11044) Bumps [com.dlsc.gemsfx:gemsfx](https://github.com/dlsc-software-consulting-gmbh/GemsFX) from 2.2.0 to 2.4.0. - [Release notes](https://github.com/dlsc-software-consulting-gmbh/GemsFX/releases) - [Changelog](https://github.com/dlsc-software-consulting-gmbh/GemsFX/blob/master/CHANGELOG.md) - [Commits](https://github.com/dlsc-software-consulting-gmbh/GemsFX/compare/v2.2.0...v2.4.0) --- updated-dependencies: - dependency-name: com.dlsc.gemsfx:gemsfx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump org.apache.pdfbox:fontbox from 3.0.1 to 3.0.2 (#11042) Bumps org.apache.pdfbox:fontbox from 3.0.1 to 3.0.2. --- updated-dependencies: - dependency-name: org.apache.pdfbox:fontbox dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Keep enclosing braces of authors (#11034) * Add test cases * Add test cases * Keep braces for last part * Refine method description * Adapt test to new braces keeping * Add CHANGELOG.md entry * Adapt tests * More edge cases * Minor code beautification * Simplify code * Fix braces removing * Extract static fields, refactor code * Fix removal of {} for export * Re-add Objects.requireNonNull * Fix typo * Re-add NPE throwing * Rename to modern terms * Consistent initialization * Improve citation relations (#11016) * Collect DOI and publication type from semantich scholar to be able to expand the information of the new entries later by search through DOI * Include abstract in the request. This lets the GUI show the abstract since that was implemented already. Refactor api request string since most of it is shared * Add button to open the relation paper's DOI URL. Fix DOI for some ArXiv entries. * Don't show the open link button if there is no link to open. * Make field value null error a bit more useful * Include SemanticScholar url in the request and use it as the URL field. * Add changes to changelog * Change tooltip text to an existing, more informative one * Run rewriter to fix pull request * improve url optional handling --------- Co-authored-by: Siedlerchr * issue #10993 - doc: updated CHANGELOG.md * fix: fixed unit tests not passing due to name changes in Author interface (#10995) * feat: changed CFFExporter to use YAML library snakeyaml instead (#10995) * feat: added support for references and ALL possible CFF fields in importer (#10995) * fix: added requested changes (#10995) + updated CHANGELOG.md + removed useless comments + refactored both CffImporter and CffExporter to use more specific methods + used a BiMap to avoid repeating mappings between CffImporter and CffExporter + copied entryMap in exporter to avoid side-effects * fix: task rewriteDryRun fixed to pass by removing test in BibEntryTest * refactor: deleted useless methods in CffImporter (#10995) * doc: added decision MADR document for cff export (#10995) * feat: add a cites or related relationship between imported entries in CffImporter (#10995) * doc: updated MADR decision document for cff export to pass markdownlint (#10995) * fix: fixed round-trip test to use mock citatioKeyPatternPreferences correctly (#10995) * fix: fixed MADR document for CFF export decision to pass Jekyll CI check (#10995) * fix: fixed requested changes (#10995) + fixed typo in CHANGELOG.md + tested multiline abstract in CFFImporter * feat: finished CFFExporter logic and crafted working round-trip test (#10995) * fix: fixed typos in MADR decision doc for CFF export and refactore ImportFormatReader signature (#10995) * Some code beautification * Use existing method getEntryLinkList * Use getEntryLinkList * Use JabRef's Date class for parsing --------- Signed-off-by: Anders Blomqvist Signed-off-by: dependabot[bot] Co-authored-by: Oliver Kopp Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Co-authored-by: Emil Hultcrantz <90456354+Frequinzy@users.noreply.github.com> Co-authored-by: Anna Maartensson <120831475+annamaartensson@users.noreply.github.com> Co-authored-by: Tian0602 <646432316@qq.com> Co-authored-by: LottaJohnsson <35195355+LottaJohnsson@users.noreply.github.com> Co-authored-by: Filippa Nilsson Co-authored-by: Filippa Nilsson <75281470+filippanilsson@users.noreply.github.com> Co-authored-by: Siedlerchr Co-authored-by: Anders Blomqvist Co-authored-by: ZOU Hetai <33616271+JXNCTED@users.noreply.github.com> Co-authored-by: Hannes Stig Co-authored-by: Elliot Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roc <1844478+ror3d@users.noreply.github.com> --- CHANGELOG.md | 1 + build.gradle | 3 + .../0029-cff-export-multiple-entries.md | 55 ++++ src/main/java/module-info.java | 1 + .../org/jabref/cli/ArgumentProcessor.java | 4 +- src/main/java/org/jabref/cli/JabRefCLI.java | 4 +- .../gui/externalfiles/ImportHandler.java | 4 +- .../jabref/gui/importer/ImportCommand.java | 8 +- .../jabref/logic/exporter/CffExporter.java | 263 +++++++++++++++++ .../logic/exporter/ExporterFactory.java | 2 +- .../logic/importer/ImportFormatReader.java | 7 +- .../importer/fileformat/CffImporter.java | 217 ++++++++++---- .../logic/integrity/EntryLinkChecker.java | 21 +- .../org/jabref/logic/layout/LayoutEntry.java | 4 - .../jabref/logic/layout/format/CffDate.java | 70 ----- .../jabref/logic/layout/format/CffType.java | 24 -- .../org/jabref/model/entry/EntryLinkList.java | 4 +- src/main/resources/l10n/JabRef_en.properties | 2 +- src/main/resources/resource/layout/cff.layout | 17 -- .../logic/exporter/CffExporterTest.java | 277 ++++++++++++++---- .../ImportFormatReaderIntegrationTest.java | 4 +- .../ImportFormatReaderParameterlessTest.java | 3 +- .../importer/fileformat/CffImporterTest.java | 142 ++++++--- .../logic/layout/format/CffDateTest.java | 45 --- .../logic/importer/fileformat/CITATION.cff | 58 ++++ .../CffImporterPreferredCitation.cff | 37 +++ .../fileformat/CffImporterReferences.cff | 47 +++ .../CffImporterTestMultilineAbstract.cff | 36 +++ 28 files changed, 1017 insertions(+), 343 deletions(-) create mode 100644 docs/decisions/0029-cff-export-multiple-entries.md create mode 100644 src/main/java/org/jabref/logic/exporter/CffExporter.java delete mode 100644 src/main/java/org/jabref/logic/layout/format/CffDate.java delete mode 100644 src/main/java/org/jabref/logic/layout/format/CffType.java delete mode 100644 src/main/resources/resource/layout/cff.layout delete mode 100644 src/test/java/org/jabref/logic/layout/format/CffDateTest.java create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CITATION.cff create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterPreferredCitation.cff create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestMultilineAbstract.cff diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e0db3dd4c..a27e119b67f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We store the citation relations in an LRU cache to avoid bloating the memory and out-of-memory exceptions. [#10958](https://github.com/JabRef/jabref/issues/10958) - Keywords field are now displayed as tags. [#10910](https://github.com/JabRef/jabref/pull/10910) - Citation relations now get more information, and have quick access to view the articles in a browser without adding them to the library [#10869](https://github.com/JabRef/jabref/issues/10869) +- Importer/Exporter for CFF format now supports JabRef `cites` and `related` relationships, as well as all fields from the CFF specification. [#10993](https://github.com/JabRef/jabref/issues/10993) ### Fixed diff --git a/build.gradle b/build.gradle index 567e2905e51..17a97635ae4 100644 --- a/build.gradle +++ b/build.gradle @@ -252,6 +252,9 @@ dependencies { // parse plist files implementation 'com.googlecode.plist:dd-plist:1.28' + // YAML formatting + implementation 'org.yaml:snakeyaml:2.2' + testImplementation 'io.github.classgraph:classgraph:4.8.168' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.platform:junit-platform-launcher:1.10.2' diff --git a/docs/decisions/0029-cff-export-multiple-entries.md b/docs/decisions/0029-cff-export-multiple-entries.md new file mode 100644 index 00000000000..179ceab745d --- /dev/null +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -0,0 +1,55 @@ +--- +nav_order: 28 +parent: Decision Records +--- + + + +# Exporting multiple entries to CFF + +## Context and Problem Statement + +The need for an [exporter](https://github.com/JabRef/jabref/issues/10661) to [CFF format](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md) raised the following issue: How to export multiple entries at once? Citation-File-Format is intended to make software and datasets citable. It should contain one "main" entry of type `software` or `dataset`, a possible preferred citation and/or several references of any type. + +## Decision Drivers + +* Make exported files compatible with official CFF tools +* Make exporting process logical for users + +## Considered Options + +* When exporting: + * Export non-`software` entries with dummy topmost `sofware` and entries as `preferred-citation` + * Export non-`software` entries with dummy topmost `sofware` and entries as `references` + * Forbid exporting multiple entries at once + * Forbid exporting more than one software entry at once + * Export entries in several files (i.e. one / file) + * Export several `software` entries with one of them topmost and all others as `references` +* Export several `software` entries with a dummy topmost `software` element and all others as `references` +* When importing: + * Only create one entry / file, enven if there is a `preferred-citation` or `references` + * Add a JabRef `cites` relation from `software` entry to its `preferred-citation` + * Add a JabRef `cites` relation from `preferred-citation` entry to the main `software` entry + * Separate `software` entries from their `preferred-citation` or `references` + +## Decision Outcome + +The decision outcome is the following. + +* When exporting, JabRef will have a different behavior depending on entries type. + * If multiple non-`software` entries are selected, then exporter uses the `references` field with a dummy topmost `software` element. + * If several entries including a `software` or `dataset` one are selected, then exporter uses this one as topmost element and the others as `references`, adding a potential `preferred-citation` for the potential `cites` element of the topmost `software` entry. + * If several entries including several `software` ones are selected, then exporter uses a dummy topmost element, and selected entries are exported as `references`. The `cites` or `related` fields won't be exported in this case. + * JabRef will not handle `cites` or `related` fields for non-`software` elements. +* When importing, JabRef will create several entries: one main entry for the `software` and other entries for the potential `preferred-citation` and `references` fields. JabRef will link main entry to the preferred citation using a `cites` from the main entry, and wil link main entry to the references using a `related` from the main entry. + +### Positive Consequences + +* Exported results comply with CFF format +* The export process is "logic" : an user who exports multiple files to CFF might find it clear that they are all marked as `references` +* Importing a CFF file and then exporting the "main" (software) created entry is consistent and will produce the same result + +### Negative Consequences + +* Importing a CFF file and then exporting one of the `preferred-citation` or the `references` created entries won't result in the same file (i.e exported file will contain a dummy topmost `software` instead of the actual `software` that was imported) +* `cites` and `related` fields of non-`software` entries are not supported diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index fd4dbe1b0c2..f10bafc0d4b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -145,4 +145,5 @@ requires de.saxsys.mvvmfx.validation; requires com.jthemedetector; requires dd.plist; + requires org.yaml.snakeyaml; } diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index d8272507450..ef203b4f3aa 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -166,7 +166,9 @@ private Optional importFile(Path file, String importFormat) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); if (!"*".equals(importFormat)) { System.out.println(Localization.lang("Importing %0", file)); diff --git a/src/main/java/org/jabref/cli/JabRefCLI.java b/src/main/java/org/jabref/cli/JabRefCLI.java index 746c9529608..3d613468af5 100644 --- a/src/main/java/org/jabref/cli/JabRefCLI.java +++ b/src/main/java/org/jabref/cli/JabRefCLI.java @@ -317,7 +317,9 @@ public static void printUsage(PreferencesService preferencesService) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - new DummyFileUpdateMonitor()); + preferencesService.getCitationKeyPatternPreferences(), + new DummyFileUpdateMonitor() + ); List> importFormats = importFormatReader .getImportFormats().stream() .map(format -> new Pair<>(format.getName(), format.getId())) diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index b7fffe46b7f..ef7ce0c5860 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -372,7 +372,9 @@ private List tryImportFormats(String data) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); UnknownFormatImport unknownFormatImport = importFormatReader.importUnknownFormat(data); return unknownFormatImport.parserResult().getDatabase().getEntries(); } catch (ImportException ex) { // ex is already localized diff --git a/src/main/java/org/jabref/gui/importer/ImportCommand.java b/src/main/java/org/jabref/gui/importer/ImportCommand.java index 104d4dedec0..8fc8c072820 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCommand.java +++ b/src/main/java/org/jabref/gui/importer/ImportCommand.java @@ -79,7 +79,9 @@ public void execute() { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); SortedSet importers = importFormatReader.getImportFormats(); FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() @@ -134,7 +136,9 @@ private ParserResult doImport(List files, Importer importFormat) throws IO ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); for (Path filename : files) { try { if (importer.isEmpty()) { diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java new file mode 100644 index 00000000000..210453e4e21 --- /dev/null +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -0,0 +1,263 @@ +package org.jabref.logic.exporter; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.Author; +import org.jabref.model.entry.AuthorList; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.Date; +import org.jabref.model.entry.field.BiblatexSoftwareField; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +public class CffExporter extends Exporter { + // Fields that are taken 1:1 from BibTeX to CFF + public static final List UNMAPPED_FIELDS = List.of( + "abbreviation", "collection-doi", "collection-title", "collection-type", "commit", "copyright", + "data-type", "database", "date-accessed", "date-downloaded", "date-published", "department", "end", + "entry", "filename", "format", "issue-date", "issue-title", "license-url", "loc-end", "loc-start", + "medium", "nihmsid", "number-volumes", "patent-states", "pmcid", "repository-artifact", "repository-code", + "scope", "section", "start", "term", "thesis-type", "volume-title", "year-original" + ); + + public static final Map FIELDS_MAP = Map.ofEntries( + Map.entry(StandardField.ABSTRACT, "abstract"), + Map.entry(StandardField.DATE, "date-released"), + Map.entry(StandardField.DOI, "doi"), + Map.entry(StandardField.KEYWORDS, "keywords"), + Map.entry(BiblatexSoftwareField.LICENSE, "license"), + Map.entry(StandardField.COMMENT, "message"), + Map.entry(BiblatexSoftwareField.REPOSITORY, "repository"), + Map.entry(StandardField.TITLE, "title"), + Map.entry(StandardField.URL, "url"), + Map.entry(StandardField.VERSION, "version"), + Map.entry(StandardField.EDITION, "edition"), + Map.entry(StandardField.ISBN, "isbn"), + Map.entry(StandardField.ISSN, "issn"), + Map.entry(StandardField.ISSUE, "issue"), + Map.entry(StandardField.JOURNAL, "journal"), + Map.entry(StandardField.MONTH, "month"), + Map.entry(StandardField.NOTE, "notes"), + Map.entry(StandardField.NUMBER, "number"), + Map.entry(StandardField.PAGES, "pages"), + Map.entry(StandardField.PUBSTATE, "status"), + Map.entry(StandardField.VOLUME, "volume"), + Map.entry(StandardField.YEAR, "year") + ); + + public static final Map TYPES_MAP = Map.ofEntries( + Map.entry(StandardEntryType.Article, "article"), + Map.entry(StandardEntryType.Book, "book"), + Map.entry(StandardEntryType.Booklet, "pamphlet"), + Map.entry(StandardEntryType.Proceedings, "conference"), + Map.entry(StandardEntryType.InProceedings, "conference-paper"), + Map.entry(StandardEntryType.Misc, "misc"), + Map.entry(StandardEntryType.Manual, "manual"), + Map.entry(StandardEntryType.Software, "software"), + Map.entry(StandardEntryType.Dataset, "dataset"), + Map.entry(StandardEntryType.Report, "report"), + Map.entry(StandardEntryType.Unpublished, "unpublished") + ); + + public CffExporter() { + super("cff", "CFF", StandardFileType.CFF); + } + + @Override + public void export(BibDatabaseContext databaseContext, Path file, List entries) throws Exception { + Objects.requireNonNull(databaseContext); + Objects.requireNonNull(file); + Objects.requireNonNull(entries); + + // Do not export if no entries to export -- avoids exports with only template text + if (entries.isEmpty()) { + return; + } + + // Make a copy of the list to avoid modifying the original list + final List entriesToTransform = new ArrayList<>(entries); + + // Set up YAML options + DumperOptions options = new DumperOptions(); + options.setWidth(Integer.MAX_VALUE); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + options.setIndentWithIndicator(true); + options.setIndicatorIndent(2); + Yaml yaml = new Yaml(options); + + BibEntry main = null; + boolean mainIsDummy = false; + int countOfSoftwareAndDataSetEntries = 0; + for (BibEntry entry : entriesToTransform) { + if (entry.getType() == StandardEntryType.Software || entry.getType() == StandardEntryType.Dataset) { + main = entry; + countOfSoftwareAndDataSetEntries++; + } + } + if (countOfSoftwareAndDataSetEntries == 1) { + // If there is only one software or dataset entry, use it as the main entry + entriesToTransform.remove(main); + } else { + // If there are no software or dataset entries, create a dummy main entry holding the given entries + main = new BibEntry(StandardEntryType.Software); + mainIsDummy = true; + } + + // Transform main entry to CFF format + Map cffData = transformEntry(main, true, mainIsDummy); + + // Preferred citation + if (main.hasField(StandardField.CITES)) { + String citeKey = main.getField(StandardField.CITES).orElse("").split(",")[0]; + List citedEntries = databaseContext.getDatabase().getEntriesByCitationKey(citeKey); + entriesToTransform.removeAll(citedEntries); + if (!citedEntries.isEmpty()) { + BibEntry citedEntry = citedEntries.getFirst(); + cffData.put("preferred-citation", transformEntry(citedEntry, false, false)); + } + } + + // References + List> related = new ArrayList<>(); + if (main.hasField(StandardField.RELATED)) { + main.getEntryLinkList(StandardField.RELATED, databaseContext.getDatabase()) + .stream() + .map(link -> link.getLinkedEntry()) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(entry -> { + related.add(transformEntry(entry, false, false)); + entriesToTransform.remove(entry); + }); + } + + // Add remaining entries as references + for (BibEntry entry : entriesToTransform) { + related.add(transformEntry(entry, false, false)); + } + if (!related.isEmpty()) { + cffData.put("references", related); + } + + try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { + yaml.dump(cffData, writer); + } catch (IOException ex) { + throw new SaveException(ex); + } + } + + private Map transformEntry(BibEntry entry, boolean main, boolean dummy) { + Map cffData = new LinkedHashMap<>(); + Map fields = new HashMap<>(entry.getFieldMap()); + + if (main) { + // Mandatory CFF version field + cffData.put("cff-version", "1.2.0"); + + // Mandatory message field + String message = fields.getOrDefault(StandardField.COMMENT, + "If you use this software, please cite it using the metadata from this file."); + cffData.put("message", message); + fields.remove(StandardField.COMMENT); + } + + // Mandatory title field + String title = fields.getOrDefault(StandardField.TITLE, "No title specified."); + cffData.put("title", title); + fields.remove(StandardField.TITLE); + + // Mandatory authors field + List authors = AuthorList.parse(fields.getOrDefault(StandardField.AUTHOR, "")) + .getAuthors(); + parseAuthors(cffData, authors); + fields.remove(StandardField.AUTHOR); + + // Type + if (!dummy) { + cffData.put("type", TYPES_MAP.getOrDefault(entry.getType(), "misc")); + } + + // Keywords + String keywords = fields.getOrDefault(StandardField.KEYWORDS, null); + if (keywords != null) { + cffData.put("keywords", keywords.split(",\\s*")); + } + fields.remove(StandardField.KEYWORDS); + + // Date + String date = fields.getOrDefault(StandardField.DATE, null); + if (date != null) { + parseDate(cffData, date); + } + fields.remove(StandardField.DATE); + + // Remaining fields not handled above + for (Field field : fields.keySet()) { + if (FIELDS_MAP.containsKey(field)) { + cffData.put(FIELDS_MAP.get(field), fields.get(field)); + } else if (field instanceof UnknownField) { + // Check that field is accepted by CFF format specification + if (UNMAPPED_FIELDS.contains(field.getName())) { + cffData.put(field.getName(), fields.get(field)); + } + } + } + return cffData; + } + + private void parseAuthors(Map data, List authors) { + List> authorsList = new ArrayList<>(); + authors.forEach(author -> { + Map authorMap = new LinkedHashMap<>(); + if (author.getFamilyName().isPresent()) { + authorMap.put("family-names", author.getFamilyName().get()); + } + if (author.getGivenName().isPresent()) { + authorMap.put("given-names", author.getGivenName().get()); + } + if (author.getNamePrefix().isPresent()) { + authorMap.put("name-particle", author.getNamePrefix().get()); + } + if (author.getNameSuffix().isPresent()) { + authorMap.put("name-suffix", author.getNameSuffix().get()); + } + authorsList.add(authorMap); + }); + data.put("authors", authorsList.isEmpty() ? List.of(Map.of("name", "/")) : authorsList); + } + + private void parseDate(Map data, String date) { + Optional parsedDateOpt = Date.parse(date); + if (parsedDateOpt.isEmpty()) { + data.put("issue-date", date); + return; + } + Date parsedDate = parsedDateOpt.get(); + if (parsedDate.getYear().isPresent() && parsedDate.getMonth().isPresent() && parsedDate.getDay().isPresent()) { + data.put("date-released", parsedDate.getNormalized()); + return; + } + parsedDate.getMonth().ifPresent(month -> data.put("month", month.getNumber())); + parsedDate.getYear().ifPresent(year -> data.put("year", year)); + } +} + diff --git a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java index 31ec6edb281..23c374c3b87 100644 --- a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java +++ b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java @@ -55,7 +55,6 @@ public static ExporterFactory create(PreferencesService preferencesService, exporters.add(new TemplateExporter("MIS Quarterly", "misq", "misq", "misq", StandardFileType.RTF, layoutPreferences, saveOrder)); exporters.add(new TemplateExporter("CSL YAML", "yaml", "yaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); exporters.add(new TemplateExporter("Hayagriva YAML", "hayagrivayaml", "hayagrivayaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); - exporters.add(new TemplateExporter("CFF", "cff", "cff", null, StandardFileType.CFF, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); exporters.add(new OpenOfficeDocumentCreator()); exporters.add(new OpenDocumentSpreadsheetCreator()); exporters.add(new MSBibExporter()); @@ -63,6 +62,7 @@ public static ExporterFactory create(PreferencesService preferencesService, exporters.add(new XmpExporter(xmpPreferences)); exporters.add(new XmpPdfExporter(xmpPreferences)); exporters.add(new EmbeddedBibFilePdfExporter(bibDatabaseMode, entryTypesManager, fieldPreferences)); + exporters.add(new CffExporter()); // Now add custom export formats exporters.addAll(customFormats); diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index c7eb32f0d38..df54b8cf52f 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -9,6 +9,7 @@ import java.util.SortedSet; import java.util.TreeSet; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.logic.importer.fileformat.BiblioscapeImporter; import org.jabref.logic.importer.fileformat.BibtexImporter; import org.jabref.logic.importer.fileformat.CffImporter; @@ -50,13 +51,15 @@ public class ImportFormatReader { private final ImporterPreferences importerPreferences; private final ImportFormatPreferences importFormatPreferences; private final FileUpdateMonitor fileUpdateMonitor; + private final CitationKeyPatternPreferences citationKeyPatternPreferences; public ImportFormatReader(ImporterPreferences importerPreferences, ImportFormatPreferences importFormatPreferences, - FileUpdateMonitor fileUpdateMonitor) { + CitationKeyPatternPreferences citationKeyPatternPreferences, FileUpdateMonitor fileUpdateMonitor) { this.importerPreferences = importerPreferences; this.importFormatPreferences = importFormatPreferences; this.fileUpdateMonitor = fileUpdateMonitor; + this.citationKeyPatternPreferences = citationKeyPatternPreferences; reset(); } @@ -82,7 +85,7 @@ public void reset() { formats.add(new RepecNepImporter(importFormatPreferences)); formats.add(new RisImporter()); formats.add(new SilverPlatterImporter()); - formats.add(new CffImporter()); + formats.add(new CffImporter(citationKeyPatternPreferences)); formats.add(new BiblioscapeImporter()); formats.add(new BibtexImporter(importFormatPreferences, fileUpdateMonitor)); formats.add(new CitaviXmlImporter()); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java index 40e52fb98da..7a180d79c38 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -6,8 +6,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import org.jabref.logic.citationkeypattern.CitationKeyGenerator; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.exporter.CffExporter; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.util.StandardFileType; @@ -18,15 +20,26 @@ import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.EntryType; import org.jabref.model.entry.types.StandardEntryType; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.collect.HashBiMap; public class CffImporter extends Importer { + public static final Map FIELDS_MAP = HashBiMap.create(CffExporter.FIELDS_MAP).inverse(); + public static final Map TYPES_MAP = HashBiMap.create(CffExporter.TYPES_MAP).inverse(); + + private final CitationKeyPatternPreferences citationKeyPatternPreferences; + + public CffImporter(CitationKeyPatternPreferences citationKeyPatternPreferences) { + this.citationKeyPatternPreferences = citationKeyPatternPreferences; + } + @Override public String getName() { return "CFF"; @@ -44,7 +57,7 @@ public String getId() { @Override public String getDescription() { - return "Importer for the CFF format. Is only used to cite software, one entry per file."; + return "Importer for the CFF format, which is intended to make software and datasets citable."; } // POJO classes for yaml data @@ -52,11 +65,20 @@ private static class CffFormat { private final HashMap values = new HashMap<>(); @JsonProperty("authors") - private List authors; + private List authors; @JsonProperty("identifiers") private List ids; + @JsonProperty("keywords") + private List keywords; + + @JsonProperty("preferred-citation") + private CffReference preferred; + + @JsonProperty("references") + private List references; + public CffFormat() { } @@ -66,10 +88,10 @@ private void setValues(String key, String value) { } } - private static class CffAuthor { + private static class CffEntity { private final HashMap values = new HashMap<>(); - public CffAuthor() { + public CffEntity() { } @JsonAnySetter @@ -88,43 +110,93 @@ public CffIdentifier() { } } + private static class CffReference { + private final HashMap values = new HashMap<>(); + + @JsonProperty("authors") + private List authors; + + @JsonProperty("conference") + private CffEntity conference; + + @JsonProperty("contact") + private CffEntity contact; + + @JsonProperty("editors") + private List editors; + + @JsonProperty("editors-series") + private List editorsSeries; + + @JsonProperty("database-provider") + private CffEntity databaseProvider; + + @JsonProperty("institution") + private CffEntity institution; + + @JsonProperty("keywords") + private List keywords; + + @JsonProperty("languages") + private List languages; + + @JsonProperty("location") + private CffEntity location; + + @JsonProperty("publisher") + private CffEntity publisher; + + @JsonProperty("recipients") + private List recipients; + + @JsonProperty("senders") + private List senders; + + @JsonProperty("translators") + private List translators; + + @JsonProperty("type") + private String type; + + public CffReference() { + } + + @JsonAnySetter + private void setValues(String key, String value) { + values.put(key, value); + } + } + @Override public ParserResult importDatabase(BufferedReader reader) throws IOException { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); CffFormat citation = mapper.readValue(reader, CffFormat.class); + List entriesList = new ArrayList<>(); + + // Remove CFF version and type + citation.values.remove("cff-version"); + + // Parse main entry HashMap entryMap = new HashMap<>(); - StandardEntryType entryType = StandardEntryType.Software; - - // Map CFF fields to JabRef Fields - HashMap fieldMap = getFieldMappings(); - for (Map.Entry property : citation.values.entrySet()) { - if (fieldMap.containsKey(property.getKey())) { - entryMap.put(fieldMap.get(property.getKey()), property.getValue()); - } else if ("type".equals(property.getKey())) { - if ("dataset".equals(property.getValue())) { - entryType = StandardEntryType.Dataset; - } - } else if (getUnmappedFields().contains(property.getKey())) { - entryMap.put(new UnknownField(property.getKey()), property.getValue()); - } - } + EntryType entryType = TYPES_MAP.getOrDefault(citation.values.get("type"), StandardEntryType.Software); + citation.values.remove("type"); // Translate CFF author format to JabRef author format - String authorStr = citation.authors.stream() - .map(author -> author.values) - .map(vals -> vals.get("name") != null ? - new Author(vals.get("name"), "", "", "", "") : - new Author(vals.get("given-names"), null, vals.get("name-particle"), - vals.get("family-names"), vals.get("name-suffix"))) - .collect(AuthorList.collect()) - .getAsFirstLastNamesWithAnd(); - entryMap.put(StandardField.AUTHOR, authorStr); + entryMap.put(StandardField.AUTHOR, parseAuthors(citation.authors)); + + // Parse keywords + if (citation.keywords != null) { + entryMap.put(StandardField.KEYWORDS, String.join(", ", citation.keywords)); + } + + // Map CFF simple fields to JabRef Fields + parseFields(citation.values, entryMap); // Select DOI to keep if ((entryMap.get(StandardField.DOI) == null) && (citation.ids != null)) { List doiIds = citation.ids.stream() .filter(id -> "doi".equals(id.type)) - .collect(Collectors.toList()); + .toList(); if (doiIds.size() == 1) { entryMap.put(StandardField.DOI, doiIds.getFirst().value); } @@ -135,7 +207,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { List swhIds = citation.ids.stream() .filter(id -> "swh".equals(id.type)) .map(id -> id.value) - .collect(Collectors.toList()); + .toList(); if (swhIds.size() == 1) { entryMap.put(BiblatexSoftwareField.SWHID, swhIds.getFirst()); @@ -143,7 +215,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { List relSwhIds = swhIds.stream() .filter(id -> id.split(":").length > 3) // quick filter for invalid swhids .filter(id -> "rel".equals(id.split(":")[2])) - .collect(Collectors.toList()); + .toList(); if (relSwhIds.size() == 1) { entryMap.put(BiblatexSoftwareField.SWHID, relSwhIds.getFirst()); } @@ -152,11 +224,39 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { BibEntry entry = new BibEntry(entryType); entry.setField(entryMap); - - List entriesList = new ArrayList<>(); entriesList.add(entry); - return new ParserResult(entriesList); + // Handle `preferred-citation` and `references` fields + BibEntry preferred = null; + List references = null; + + if (citation.preferred != null) { + preferred = parseEntry(citation.preferred); + entriesList.add(preferred); + } + + if (citation.references != null) { + references = citation.references.stream().map(this::parseEntry).toList(); + entriesList.addAll(references); + } + + ParserResult res = new ParserResult(entriesList); + CitationKeyGenerator gen = new CitationKeyGenerator(res.getDatabaseContext(), citationKeyPatternPreferences); + + if (preferred != null) { + gen.generateAndSetKey(preferred); + entry.setField(StandardField.CITES, preferred.getCitationKey().orElse("")); + } + + if (references != null) { + references.forEach(ref -> { + gen.generateAndSetKey(ref); + String citeKey = ref.getCitationKey().orElse(""); + String related = entry.getField(StandardField.RELATED).orElse(""); + entry.setField(StandardField.RELATED, related.isEmpty() ? citeKey : related + "," + citeKey); + }); + } + return res; } @Override @@ -173,29 +273,34 @@ public boolean isRecognizedFormat(BufferedReader reader) throws IOException { } } - private HashMap getFieldMappings() { - HashMap fieldMappings = new HashMap<>(); - fieldMappings.put("title", StandardField.TITLE); - fieldMappings.put("version", StandardField.VERSION); - fieldMappings.put("doi", StandardField.DOI); - fieldMappings.put("license", BiblatexSoftwareField.LICENSE); - fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); - fieldMappings.put("url", StandardField.URL); - fieldMappings.put("abstract", StandardField.ABSTRACT); - fieldMappings.put("message", StandardField.COMMENT); - fieldMappings.put("date-released", StandardField.DATE); - fieldMappings.put("keywords", StandardField.KEYWORDS); - return fieldMappings; + private String parseAuthors(List authors) { + return authors.stream() + .map(author -> author.values) + .map(vals -> vals.get("name") != null ? + new Author(vals.get("name"), "", "", "", "") : + new Author(vals.get("given-names"), null, vals.get("name-particle"), + vals.get("family-names"), vals.get("name-suffix"))) + .collect(AuthorList.collect()) + .getAsFirstLastNamesWithAnd(); } - private List getUnmappedFields() { - List fields = new ArrayList<>(); - - fields.add("commit"); - fields.add("license-url"); - fields.add("repository-code"); - fields.add("repository-artifact"); + private BibEntry parseEntry(CffReference reference) { + Map entryMap = new HashMap<>(); + EntryType entryType = TYPES_MAP.getOrDefault(reference.type, StandardEntryType.Article); + entryMap.put(StandardField.AUTHOR, parseAuthors(reference.authors)); + parseFields(reference.values, entryMap); + BibEntry entry = new BibEntry(entryType); + entry.setField(entryMap); + return entry; + } - return fields; + private void parseFields(Map values, Map entryMap) { + for (Map.Entry property : values.entrySet()) { + if (FIELDS_MAP.containsKey(property.getKey())) { + entryMap.put(FIELDS_MAP.get(property.getKey()), property.getValue()); + } else { + entryMap.put(new UnknownField(property.getKey()), property.getValue()); + } + } } } diff --git a/src/main/java/org/jabref/logic/integrity/EntryLinkChecker.java b/src/main/java/org/jabref/logic/integrity/EntryLinkChecker.java index ccf64cd858b..8827e8a2169 100644 --- a/src/main/java/org/jabref/logic/integrity/EntryLinkChecker.java +++ b/src/main/java/org/jabref/logic/integrity/EntryLinkChecker.java @@ -1,7 +1,6 @@ package org.jabref.logic.integrity; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map.Entry; import java.util.Objects; @@ -26,20 +25,12 @@ public List check(BibEntry entry) { List result = new ArrayList<>(); for (Entry field : entry.getFieldMap().entrySet()) { Set properties = field.getKey().getProperties(); - if (properties.contains(FieldProperty.SINGLE_ENTRY_LINK)) { - if (database.getEntryByCitationKey(field.getValue()).isEmpty()) { - result.add(new IntegrityMessage(Localization.lang("Referenced citation key does not exist"), entry, - field.getKey())); - } - } else if (properties.contains(FieldProperty.MULTIPLE_ENTRY_LINK)) { - List keys = new ArrayList<>(Arrays.asList(field.getValue().split(","))); - for (String key : keys) { - if (database.getEntryByCitationKey(key).isEmpty()) { - result.add(new IntegrityMessage( - Localization.lang("Referenced citation key does not exist") + ": " + key, entry, - field.getKey())); - } - } + if (properties.contains(FieldProperty.MULTIPLE_ENTRY_LINK) || properties.contains(FieldProperty.SINGLE_ENTRY_LINK)) { + entry.getEntryLinkList(field.getKey(), database).stream() + .filter(parsedEntryLink -> parsedEntryLink.getLinkedEntry().isEmpty()) + .forEach(parsedEntryLink -> result.add(new IntegrityMessage( + Localization.lang("Referenced citation key '%0' does not exist", parsedEntryLink.getKey()), + entry, field.getKey()))); } } return result; diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index 211d9b02172..7d0cf3d3a4d 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -35,8 +35,6 @@ import org.jabref.logic.layout.format.AuthorOrgSci; import org.jabref.logic.layout.format.Authors; import org.jabref.logic.layout.format.CSLType; -import org.jabref.logic.layout.format.CffDate; -import org.jabref.logic.layout.format.CffType; import org.jabref.logic.layout.format.CompositeFormat; import org.jabref.logic.layout.format.CreateBibORDFAuthors; import org.jabref.logic.layout.format.CreateDocBook4Authors; @@ -488,8 +486,6 @@ private LayoutFormatter getLayoutFormatterByName(String name) { case "ShortMonth" -> new ShortMonthFormatter(); case "ReplaceWithEscapedDoubleQuotes" -> new ReplaceWithEscapedDoubleQuotes(); case "HayagrivaType" -> new HayagrivaType(); - case "CffType" -> new CffType(); - case "CffDate" -> new CffDate(); default -> null; }; } diff --git a/src/main/java/org/jabref/logic/layout/format/CffDate.java b/src/main/java/org/jabref/logic/layout/format/CffDate.java deleted file mode 100644 index 1e81697a049..00000000000 --- a/src/main/java/org/jabref/logic/layout/format/CffDate.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.jabref.logic.layout.format; - -import java.time.LocalDate; -import java.time.Year; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -import org.jabref.logic.layout.LayoutFormatter; -import org.jabref.logic.util.OS; - -/** - * This class is used to parse dates for CFF exports. Since we do not know if the input String contains - * year, month and day, we must go through all these cases to return the best CFF format possible. - * Different cases are stated below. - *

- * Year, Month and Day contained => preferred-citation: - * date-released: yyyy-mm-dd - *

- * Year and Month contained => preferred-citation - * ... - * month: mm - * year: yyyy - *

- * Year contained => preferred-citation: - * ... - * year: yyyy - *

- * Poorly formatted => preferred-citation: - * ... - * issue-date: text-as-is - */ -public class CffDate implements LayoutFormatter { - @Override - public String format(String fieldText) { - StringBuilder builder = new StringBuilder(); - String formatString = "yyyy-MM-dd"; - try { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - LocalDate date = LocalDate.parse(fieldText, DateTimeFormatter.ISO_LOCAL_DATE); - builder.append("date-released: "); - builder.append(date.format(formatter)); - } catch (DateTimeParseException e) { - try { - formatString = "yyyy-MM"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - YearMonth yearMonth = YearMonth.parse(fieldText, formatter); - int month = yearMonth.getMonth().getValue(); - int year = yearMonth.getYear(); - builder.append("month: "); - builder.append(month); - builder.append(OS.NEWLINE); - builder.append(" year: "); // Account for indent since we are in `preferred-citation` indentation block - builder.append(year); - } catch (DateTimeParseException f) { - try { - formatString = "yyyy"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - int year = Year.parse(fieldText, formatter).getValue(); - builder.append("year: "); - builder.append(year); - } catch (DateTimeParseException g) { - builder.append("issue-date: "); - builder.append(fieldText); - } - } - } - return builder.toString(); - } -} diff --git a/src/main/java/org/jabref/logic/layout/format/CffType.java b/src/main/java/org/jabref/logic/layout/format/CffType.java deleted file mode 100644 index 5de168b77ba..00000000000 --- a/src/main/java/org/jabref/logic/layout/format/CffType.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.jabref.logic.layout.format; - -import org.jabref.logic.layout.LayoutFormatter; -import org.jabref.model.entry.types.StandardEntryType; - -public class CffType implements LayoutFormatter { - @Override - public String format(String value) { - return switch (StandardEntryType.valueOf(value)) { - case Article, Conference -> "article"; - case Book -> "book"; - case Booklet -> "pamphlet"; - case InProceedings -> "conference-paper"; - case Proceedings -> "proceedings"; - case Misc -> "misc"; - case Manual -> "manual"; - case Software -> "software"; - case Report, TechReport -> "report"; - case Unpublished -> "unpublished"; - default -> "generic"; - }; - } -} - diff --git a/src/main/java/org/jabref/model/entry/EntryLinkList.java b/src/main/java/org/jabref/model/entry/EntryLinkList.java index 9b468f27e81..d17fb19720f 100644 --- a/src/main/java/org/jabref/model/entry/EntryLinkList.java +++ b/src/main/java/org/jabref/model/entry/EntryLinkList.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import org.jabref.model.database.BibDatabase; +import org.jabref.model.strings.StringUtil; public class EntryLinkList { @@ -15,9 +16,8 @@ private EntryLinkList() { public static List parse(String fieldValue, BibDatabase database) { List result = new ArrayList<>(); - if ((fieldValue != null) && !fieldValue.isEmpty()) { + if (!StringUtil.isNullOrEmpty(fieldValue)) { String[] entries = fieldValue.split(SEPARATOR); - for (String entry : entries) { result.add(new ParsedEntryLink(entry, database)); } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index b58183e9a6d..90a6a07a81d 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -1774,7 +1774,7 @@ Entered\ database\ has\ obsolete\ structure\ and\ is\ no\ longer\ supported.=Ent However,\ a\ new\ database\ was\ created\ alongside\ the\ pre-3.6\ one.=However, a new database was created alongside the pre-3.6 one. Opens\ a\ link\ where\ the\ current\ development\ version\ can\ be\ downloaded=Opens a link where the current development version can be downloaded See\ what\ has\ been\ changed\ in\ the\ JabRef\ versions=See what has been changed in the JabRef versions -Referenced\ citation\ key\ does\ not\ exist=Referenced citation key does not exist +Referenced\ citation\ key\ '%0'\ does\ not\ exist=Referenced citation key '%0' does not exist Full\ text\ document\ for\ entry\ %0\ already\ linked.=Full text document for entry %0 already linked. Download\ full\ text\ documents=Download full text documents You\ are\ about\ to\ download\ full\ text\ documents\ for\ %0\ entries.=You are about to download full text documents for %0 entries. diff --git a/src/main/resources/resource/layout/cff.layout b/src/main/resources/resource/layout/cff.layout deleted file mode 100644 index 7ed334cdd07..00000000000 --- a/src/main/resources/resource/layout/cff.layout +++ /dev/null @@ -1,17 +0,0 @@ -cff-version: 1.2.0 -message: "If you use this, please cite the work from preferred-citation." -authors: - - name: \format[Default(No author specified.)]{\author} -title: \format[Default(No title specified.)]{\title} -preferred-citation: - type: \format[CffType, Default(generic)]{\entrytype} - authors: - - name: \format[Default(No author specified.)]{\author} - title: \format[Default(No title specified.)]{\title} -\begin{date} - \format[CffDate]{\date} -\end{date} -\begin{abstract} abstract: \abstract\end{abstract} -\begin{doi} doi: \doi\end{doi} -\begin{volume} volume: \volume\end{volume} -\begin{url} url: "\url"\end{url} diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index f9314e11b56..f0daa7e0bd4 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -1,17 +1,23 @@ + package org.jabref.logic.exporter; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.jabref.logic.layout.LayoutFormatterPreferences; -import org.jabref.logic.util.StandardFileType; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; +import org.jabref.logic.importer.fileformat.CffImporter; +import org.jabref.logic.importer.fileformat.CffImporterTest; +import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.model.metadata.SaveOrder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -20,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CffExporterTest { @@ -28,132 +35,282 @@ public class CffExporterTest { @BeforeAll static void setUp() { - cffExporter = new TemplateExporter( - "CFF", - "cff", - "cff", - null, - StandardFileType.CFF, - mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), - SaveOrder.getDefaultSaveOrder(), - BlankLineBehaviour.DELETE_BLANKS); - + cffExporter = new CffExporter(); databaseContext = new BibDatabaseContext(); } @Test - public final void exportForNoEntriesWritesNothing(@TempDir Path tempFile) throws Exception { - Path file = tempFile.resolve("ThisIsARandomlyNamedFile"); + public final void exportForNoEntriesWritesNothing(@TempDir Path tempDir) throws Exception { + Path file = tempDir.resolve("ThisIsARandomlyNamedFile"); Files.createFile(file); - cffExporter.export(databaseContext, tempFile, Collections.emptyList()); + cffExporter.export(databaseContext, tempDir, Collections.emptyList()); assertEquals(Collections.emptyList(), Files.readAllLines(file)); } @Test - public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception { + public final void exportsCorrectContent(@TempDir Path tempDir) throws Exception { BibEntry entry = new BibEntry(StandardEntryType.Article) .withCitationKey("test") .withField(StandardField.AUTHOR, "Test Author") .withField(StandardField.TITLE, "Test Title") .withField(StandardField.URL, "http://example.com"); - Path file = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", - "authors:", - " - name: Test Author", - "title: Test Title", - "preferred-citation:", - " type: article", - " authors:", - " - name: Test Author", - " title: Test Title", - " url: \"http://example.com\""); + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: No title specified.", + "authors:", + " - name: /", + "references:", + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: article", + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } @Test - public final void usesCorrectType(@TempDir Path tempFile) throws Exception { + public final void usesCorrectType(@TempDir Path tempDir) throws Exception { BibEntry entry = new BibEntry(StandardEntryType.InProceedings) .withCitationKey("test") .withField(StandardField.AUTHOR, "Test Author") .withField(StandardField.TITLE, "Test Title") .withField(StandardField.DOI, "random_doi_value"); - Path file = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", + "message: If you use this software, please cite it using the metadata from this file.", + "title: No title specified.", + "authors:", + " - name: /", + "references:", + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: conference-paper", + " doi: random_doi_value"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void usesCorrectDefaultValues(@TempDir Path tempDir) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Thesis).withCitationKey("test"); + + Path file = tempDir.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: No title specified.", "authors:", - " - name: Test Author", + " - name: /", + "references:", + " - title: No title specified.", + " authors:", + " - name: /", + " type: misc"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void exportsSoftwareCorrectly(@TempDir Path tempDir) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Software) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DOI, "random_doi_value"); + + Path file = tempDir.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", "title: Test Title", - "preferred-citation:", - " type: conference-paper", - " authors:", - " - name: Test Author", - " title: Test Title", - " doi: random_doi_value"); + "authors:", + " - family-names: Author", + " given-names: Test", + "type: software", + "doi: random_doi_value"); assertEquals(expected, Files.readAllLines(file)); } @Test - public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Exception { - BibEntry entry = new BibEntry(StandardEntryType.Thesis) - .withCitationKey("test"); + public final void exportsSoftwareDateCorrectly(@TempDir Path tempDir) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Software) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DATE, "2003-11-06"); - Path file = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", + "message: If you use this software, please cite it using the metadata from this file.", + "title: Test Title", "authors:", - " - name: No author specified.", + " - family-names: Author", + " given-names: Test", + "type: software", + "date-released: '2003-11-06'"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void exportsArticleDateCorrectly(@TempDir Path tempDir) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DATE, "2003-11"); + + Path file = tempDir.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", "title: No title specified.", - "preferred-citation:", - " type: generic", - " authors:", - " - name: No author specified.", - " title: No title specified."); + "authors:", + " - name: /", + "references:", + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: article", + " month: 11", + " year: 2003"); assertEquals(expected, Files.readAllLines(file)); } @Test - void passesModifiedCharset(@TempDir Path tempFile) throws Exception { + public final void passesModifiedCharset(@TempDir Path tempDir) throws Exception { BibEntry entry = new BibEntry(StandardEntryType.Article) .withCitationKey("test") .withField(StandardField.AUTHOR, "谷崎 潤一郎") .withField(StandardField.TITLE, "細雪") .withField(StandardField.URL, "http://example.com"); - Path file = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", + "message: If you use this software, please cite it using the metadata from this file.", + "title: No title specified.", + "authors:", + " - name: /", + "references:", + " - title: 細雪", + " authors:", + " - family-names: 潤一郎", + " given-names: 谷崎", + " type: article", + " url: http://example.com"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void roundTripTest(@TempDir Path tempDir) throws Exception { + CitationKeyPatternPreferences citationKeyPatternPreferences = mock( + CitationKeyPatternPreferences.class, + Answers.RETURNS_SMART_NULLS + ); + when(citationKeyPatternPreferences.getKeyPattern()) + .thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); + + // First, import the file which will be parsed as two entries + CffImporter importer = new CffImporter(citationKeyPatternPreferences); + Path file = Path.of(CffImporterTest.class.getResource("CITATION.cff").toURI()); + BibDatabase db = importer.importDatabase(file).getDatabase(); + BibDatabaseContext dbc = new BibDatabaseContext(db); + + // Then, export both entries that will be exported as one file + Path out = tempDir.resolve("OUT.cff"); + Files.createFile(out); + cffExporter.export(dbc, out, db.getEntries()); + + Set expectedSoftware = Set.of( + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: JabRef", "authors:", - " - name: 谷崎 潤一郎", - "title: 細雪", + " - family-names: Kopp", + " given-names: Oliver", + " - family-names: Diez", + " given-names: Tobias", + " - family-names: Schwentker", + " given-names: Christoph", + " - family-names: Snethlage", + " given-names: Carl Christian", + " - family-names: Asketorp", + " given-names: Jonatan", + " - family-names: Tutzer", + " given-names: Benedikt", + " - family-names: Ertel", + " given-names: Thilo", + " - family-names: Nasri", + " given-names: Houssem", + "type: software", + "keywords:", + " - reference manager", + " - bibtex", + " - biblatex", + "license: MIT", + "repository-code: https://github.com/jabref/jabref/", + "abstract: JabRef is an open-source, cross-platform citation and reference management tool.", + "url: https://www.jabref.org", "preferred-citation:", - " type: article", + " title: 'JabRef: BibTeX-based literature management software'", " authors:", - " - name: 谷崎 潤一郎", - " title: 細雪", - " url: \"http://example.com\""); + " - family-names: Kopp", + " given-names: Oliver", + " - family-names: Snethlage", + " given-names: Carl Christian", + " - family-names: Schwentker", + " given-names: Christoph", + " type: article", + " month: '11'", + " issue: '138'", + " volume: '44'", + " year: '2023'", + " doi: 10.47397/tb/44-3/tb138kopp-jabref", + " journal: TUGboat", + " number: '3'", + " start: '441'", + " end: '447'"); - assertEquals(expected, Files.readAllLines(file)); + // Tests equality of sets since last lines order is random and relies on entries internal order + try (Stream st = Files.lines(out)) { + assertEquals(expectedSoftware, st.collect(Collectors.toSet())); + } } } + diff --git a/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java b/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java index 0fa6623fe6c..7bbef2de9b4 100644 --- a/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java +++ b/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java @@ -8,6 +8,7 @@ import javafx.collections.FXCollections; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.model.util.DummyFileUpdateMonitor; import org.junit.jupiter.api.BeforeEach; @@ -30,7 +31,8 @@ void setUp() { reader = new ImportFormatReader( importerPreferences, mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), - new DummyFileUpdateMonitor()); + mock(CitationKeyPatternPreferences.class), new DummyFileUpdateMonitor() + ); } @ParameterizedTest diff --git a/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java b/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java index f05facf0dd0..39f3fcc2f2e 100644 --- a/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java +++ b/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java @@ -4,6 +4,7 @@ import javafx.collections.FXCollections; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.model.util.FileUpdateMonitor; @@ -25,7 +26,7 @@ void setUp() { ImporterPreferences importerPreferences = mock(ImporterPreferences.class, Answers.RETURNS_DEEP_STUBS); when(importerPreferences.getCustomImporters()).thenReturn(FXCollections.emptyObservableSet()); ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); - reader = new ImportFormatReader(importerPreferences, importFormatPreferences, fileMonitor); + reader = new ImportFormatReader(importerPreferences, importFormatPreferences, mock(CitationKeyPatternPreferences.class), fileMonitor); } @Test diff --git a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java index 54cbfee44bf..b9f0dedbcd4 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -6,6 +6,8 @@ import java.util.Arrays; import java.util.List; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; import org.jabref.logic.util.StandardFileType; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.BiblatexSoftwareField; @@ -15,10 +17,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CffImporterTest { @@ -26,7 +31,13 @@ public class CffImporterTest { @BeforeEach public void setUp() { - importer = new CffImporter(); + CitationKeyPatternPreferences citationKeyPatternPreferences = mock( + CitationKeyPatternPreferences.class, + Answers.RETURNS_SMART_NULLS + ); + when(citationKeyPatternPreferences.getKeyPattern()) + .thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); + importer = new CffImporter(citationKeyPatternPreferences); } @Test @@ -46,7 +57,7 @@ public void sGetExtensions() { @Test public void getDescription() { - assertEquals("Importer for the CFF format. Is only used to cite software, one entry per file.", + assertEquals("Importer for the CFF format, which is intended to make software and datasets citable.", importer.getDescription()); } @@ -59,7 +70,6 @@ public void isRecognizedFormat() throws IOException, URISyntaxException { @Test public void isRecognizedFormatReject() throws IOException, URISyntaxException { List list = Arrays.asList("CffImporterTestInvalid1.cff", "CffImporterTestInvalid2.cff"); - for (String string : list) { Path file = Path.of(CffImporterTest.class.getResource(string).toURI()); assertFalse(importer.isRecognizedFormat(file)); @@ -71,9 +81,7 @@ public void importEntriesBasic() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValid.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry entry = bibEntries.getFirst(); - BibEntry expected = getPopulatedEntry().withField(StandardField.AUTHOR, "Joe van Smith"); - assertEquals(entry, expected); } @@ -82,9 +90,7 @@ public void importEntriesMultipleAuthors() throws IOException, URISyntaxExceptio Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidMultAuthors.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry entry = bibEntries.getFirst(); - BibEntry expected = getPopulatedEntry(); - assertEquals(entry, expected); } @@ -93,9 +99,8 @@ public void importEntriesSwhIdSelect1() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidSwhIdSelect1.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry entry = bibEntries.getFirst(); - - BibEntry expected = getPopulatedEntry().withField(BiblatexSoftwareField.SWHID, "swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f"); - + BibEntry expected = getPopulatedEntry() + .withField(BiblatexSoftwareField.SWHID, "swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f"); assertEquals(entry, expected); } @@ -104,61 +109,120 @@ public void importEntriesSwhIdSelect2() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidSwhIdSelect2.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry entry = bibEntries.getFirst(); - - BibEntry expected = getPopulatedEntry().withField(BiblatexSoftwareField.SWHID, "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2"); - + BibEntry expected = getPopulatedEntry() + .withField(BiblatexSoftwareField.SWHID, "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2"); assertEquals(entry, expected); } @Test public void importEntriesDataset() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestDataset.cff").toURI()); - List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); - BibEntry entry = bibEntries.getFirst(); - + BibEntry entry = importer.importDatabase(file).getDatabase().getEntries().getFirst(); BibEntry expected = getPopulatedEntry(); expected.setType(StandardEntryType.Dataset); - assertEquals(entry, expected); } @Test public void importEntriesDoiSelect() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestDoiSelect.cff").toURI()); - List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); - BibEntry entry = bibEntries.getFirst(); - + BibEntry entry = importer.importDatabase(file).getDatabase().getEntries().getFirst(); BibEntry expected = getPopulatedEntry(); - assertEquals(entry, expected); } @Test public void importEntriesUnknownFields() throws IOException, URISyntaxException { Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestUnknownFields.cff").toURI()); - List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); - BibEntry entry = bibEntries.getFirst(); - + BibEntry entry = importer.importDatabase(file).getDatabase().getEntries().getFirst(); BibEntry expected = getPopulatedEntry().withField(new UnknownField("commit"), "10ad"); + assertEquals(entry, expected); + } + @Test + public void importEntriesMultilineAbstract() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestMultilineAbstract.cff").toURI()); + BibEntry entry = importer.importDatabase(file).getDatabase().getEntries().getFirst(); + BibEntry expected = getPopulatedEntry().withField(StandardField.ABSTRACT, + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi vel tortor sem. Suspendisse posuere nibh commodo nunc iaculis, + sed eleifend justo malesuada. Curabitur sodales auctor cursus. + Fusce non elit elit. Mauris sollicitudin lobortis pulvinar. + Nullam vel enim quis tellus pellentesque sagittis non at justo. + Nam convallis et velit non auctor. Praesent id ex eros. Nullam + ullamcorper leo vitae leo rhoncus porta. In lobortis rhoncus nisl, + sit amet aliquet elit cursus ut. Cras laoreet justo in tortor vehicula, + quis semper tortor maximus. Nulla vitae ante ullamcorper, viverra + est at, laoreet tortor. Suspendisse rutrum hendrerit est in commodo. + Aenean urna purus, lobortis a condimentum et, varius ut augue. + Praesent ac lectus id mi posuere elementum. + """); assertEquals(entry, expected); } + @Test + public void importEntriesPreferredCitation() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterPreferredCitation.cff").toURI()); + List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); + + BibEntry mainEntry = bibEntries.getFirst(); + BibEntry preferredEntry = bibEntries.getLast(); + String citeKey = preferredEntry.getCitationKey().orElse(""); + + BibEntry expectedMain = getPopulatedEntry().withField(StandardField.CITES, citeKey); + + BibEntry expectedPreferred = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey(citeKey) + .withField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr.") + .withField(StandardField.DOI, "10.0001/TEST") + .withField(StandardField.URL, "www.github.com"); + + assertEquals(mainEntry, expectedMain); + assertEquals(preferredEntry, expectedPreferred); + } + + @Test + public void importEntriesReferences() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterReferences.cff").toURI()); + List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); + BibEntry mainEntry = bibEntries.getFirst(); + BibEntry referenceEntry1 = bibEntries.get(1); + BibEntry referenceEntry2 = bibEntries.getLast(); + String citeKey1 = referenceEntry1.getCitationKey().orElse(""); + String citeKey2 = referenceEntry2.getCitationKey().orElse(""); + + BibEntry expectedMain = getPopulatedEntry().withField(StandardField.RELATED, citeKey1 + "," + citeKey2); + + BibEntry expectedReference1 = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey(citeKey1) + .withField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr.") + .withField(StandardField.YEAR, "2007") + .withField(StandardField.DOI, "10.0001/TEST") + .withField(StandardField.URL, "www.example.com"); + + BibEntry expectedReference2 = new BibEntry(StandardEntryType.Manual) + .withCitationKey(citeKey2) + .withField(StandardField.AUTHOR, "Arthur Clark, Jr. and Luca von Diamond") + .withField(StandardField.DOI, "10.0002/TEST") + .withField(StandardField.URL, "www.facebook.com"); + + assertEquals(mainEntry, expectedMain); + assertEquals(referenceEntry1, expectedReference1); + assertEquals(referenceEntry2, expectedReference2); + } + public BibEntry getPopulatedEntry() { - BibEntry entry = new BibEntry(); - entry.setType(StandardEntryType.Software); - - entry.setField(StandardField.AUTHOR, "Joe van Smith and Bob Jones, Jr."); - entry.setField(StandardField.TITLE, "Test"); - entry.setField(StandardField.URL, "www.google.com"); - entry.setField(BiblatexSoftwareField.REPOSITORY, "www.github.com"); - entry.setField(StandardField.DOI, "10.0000/TEST"); - entry.setField(StandardField.DATE, "2000-07-02"); - entry.setField(StandardField.COMMENT, "Test entry."); - entry.setField(StandardField.ABSTRACT, "Test abstract."); - entry.setField(BiblatexSoftwareField.LICENSE, "MIT"); - entry.setField(StandardField.VERSION, "1.0"); - - return entry; + return new BibEntry(StandardEntryType.Software) + .withField(StandardField.AUTHOR, "Joe van Smith and Bob Jones, Jr.") + .withField(StandardField.TITLE, "Test") + .withField(StandardField.URL, "www.google.com") + .withField(BiblatexSoftwareField.REPOSITORY, "www.github.com") + .withField(StandardField.DOI, "10.0000/TEST") + .withField(StandardField.DATE, "2000-07-02") + .withField(StandardField.COMMENT, "Test entry.") + .withField(StandardField.ABSTRACT, "Test abstract.") + .withField(BiblatexSoftwareField.LICENSE, "MIT") + .withField(StandardField.VERSION, "1.0"); } } diff --git a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java deleted file mode 100644 index fd73612bbe4..00000000000 --- a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.jabref.logic.layout.format; - -import org.jabref.logic.layout.LayoutFormatter; -import org.jabref.logic.util.OS; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class CffDateTest { - - private LayoutFormatter formatter; - private String newLine; - - @BeforeEach - public void setUp() { - formatter = new CffDate(); - newLine = OS.NEWLINE; - } - - @Test - public void dayMonthYear() { - String expected = "date-released: 2003-11-06"; - assertEquals(expected, formatter.format("2003-11-06")); - } - - @Test - public void monthYear() { - String expected = "month: 7" + newLine + " " + "year: 2016"; - assertEquals(expected, formatter.format("2016-07")); - } - - @Test - public void year() { - String expected = "year: 2021"; - assertEquals(expected, formatter.format("2021")); - } - - @Test - public void poorlyFormatted() { - String expected = "issue-date: -2023"; - assertEquals(expected, formatter.format("-2023")); - } -} diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CITATION.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CITATION.cff new file mode 100644 index 00000000000..570a4e57b0c --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CITATION.cff @@ -0,0 +1,58 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: JabRef +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Oliver + family-names: Kopp + orcid: 'https://orcid.org/0000-0001-6962-4290' + - given-names: Tobias + family-names: Diez + orcid: 'https://orcid.org/0000-0002-1407-7696' + - given-names: Christoph + family-names: Schwentker + - given-names: Carl Christian + family-names: Snethlage + - given-names: Jonatan + family-names: Asketorp + - given-names: Benedikt + family-names: Tutzer + - given-names: Thilo + family-names: Ertel + - given-names: Houssem + family-names: Nasri +repository-code: 'https://github.com/jabref/jabref/' +url: 'https://www.jabref.org' +abstract: >- + JabRef is an open-source, cross-platform citation and + reference management tool. +keywords: + - reference manager + - bibtex + - biblatex +license: MIT +preferred-citation: + type: article + authors: + - family-names: "Kopp" + given-names: "Oliver" + orcid: "https://orcid.org/0000-0001-6962-4290" + - family-names: "Snethlage" + given-names: "Carl Christian" + - family-names: "Schwentker" + given-names: "Christoph" + doi: "10.47397/tb/44-3/tb138kopp-jabref" + journal: "TUGboat" + month: 11 + start: 441 + end: 447 + title: "JabRef: BibTeX-based literature management software" + issue: 138 + volume: 44 + number: 3 + year: 2023 diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterPreferredCitation.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterPreferredCitation.cff new file mode 100644 index 00000000000..107cc350306 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterPreferredCitation.cff @@ -0,0 +1,37 @@ +# YAML 1.2 +--- +abstract: "Test abstract." +authors: + - + family-names: Smith + given-names: Joe + name-particle: van + - + family-names: Jones + given-names: Bob + name-suffix: Jr. +cff-version: "1.1.0" +date-released: 2000-07-02 +doi: "10.0000/TEST" +identifiers: +license: MIT +message: "Test entry." +title: Test +version: "1.0" +url: "www.google.com" +repository: "www.github.com" +preferred-citation: + type: conference-paper + authors: + - + family-names: Duke + given-names: Jonathan + name-particle: von + - + family-names: Kingston + given-names: Jim + name-suffix: Jr. + doi: "10.0001/TEST" + url: "www.github.com" + +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff new file mode 100644 index 00000000000..dee78017ab4 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff @@ -0,0 +1,47 @@ +# YAML 1.2 +--- +abstract: "Test abstract." +authors: + - + family-names: Smith + given-names: Joe + name-particle: van + - + family-names: Jones + given-names: Bob + name-suffix: Jr. +cff-version: "1.2.0" +date-released: 2000-07-02 +identifiers: + - + type: doi + value: "10.0000/TEST" +license: MIT +message: "Test entry." +title: Test +version: "1.0" +url: "www.google.com" +repository: "www.github.com" +type: software +references: + - type: conference-paper + authors: + - family-names: Duke + given-names: Jonathan + name-particle: von + - family-names: Kingston + given-names: Jim + name-suffix: Jr. + year: 2007 + doi: 10.0001/TEST + url: www.example.com + - type: manual + authors: + - family-names: Clark + given-names: Arthur + name-suffix: Jr. + - family-names: Diamond + given-names: Luca + name-particle: von + doi: 10.0002/TEST + url: www.facebook.com diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestMultilineAbstract.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestMultilineAbstract.cff new file mode 100644 index 00000000000..4493e8d6770 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestMultilineAbstract.cff @@ -0,0 +1,36 @@ +# YAML 1.2 +--- +abstract: | + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi vel tortor sem. Suspendisse posuere nibh commodo nunc iaculis, + sed eleifend justo malesuada. Curabitur sodales auctor cursus. + Fusce non elit elit. Mauris sollicitudin lobortis pulvinar. + Nullam vel enim quis tellus pellentesque sagittis non at justo. + Nam convallis et velit non auctor. Praesent id ex eros. Nullam + ullamcorper leo vitae leo rhoncus porta. In lobortis rhoncus nisl, + sit amet aliquet elit cursus ut. Cras laoreet justo in tortor vehicula, + quis semper tortor maximus. Nulla vitae ante ullamcorper, viverra + est at, laoreet tortor. Suspendisse rutrum hendrerit est in commodo. + Aenean urna purus, lobortis a condimentum et, varius ut augue. + Praesent ac lectus id mi posuere elementum. +authors: + - + family-names: Smith + given-names: Joe + name-particle: van + - + family-names: Jones + given-names: Bob + name-suffix: Jr. +cff-version: "1.1.0" +date-released: 2000-07-02 +doi: "10.0000/TEST" +identifiers: +license: MIT +message: "Test entry." +title: Test +version: "1.0" +url: "www.google.com" +repository: "www.github.com" +... +