Skip to content

Commit a1b6d38

Browse files
jeanprbtkopporcalixtusFrequinzyannamaartensson
authored
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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * Refactor newline to match main branch Co-authored-by: Filippa Nilsson <[email protected]> * 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 <[email protected]> Co-authored-by: Tian0602 <[email protected]> Co-authored-by: LottaJohnsson <[email protected]> Co-authored-by: Filippa Nilsson <[email protected]> Co-authored-by: Filippa Nilsson <[email protected]> Co-authored-by: Oliver Kopp <[email protected]> Co-authored-by: Siedlerchr <[email protected]> * 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 <[email protected]> * [Copy] New method for serializing string constants (#12) Signed-off-by: Anders Blomqvist <[email protected]> * 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#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#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 <[email protected]> Co-authored-by: Anders Blomqvist <[email protected]> Co-authored-by: ZOU Hetai <[email protected]> Co-authored-by: Hannes Stig <[email protected]> Co-authored-by: Elliot <[email protected]> Co-authored-by: Oliver Kopp <[email protected]> * 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](GitTools/actions@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] <[email protected]> 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](3breadt/dd-plist@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] <[email protected]> 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] <[email protected]> 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](dlsc-software-consulting-gmbh/GemsFX@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] <[email protected]> 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] <[email protected]> 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 <[email protected]> * 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 <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Oliver Kopp <[email protected]> Co-authored-by: Carl Christian Snethlage <[email protected]> Co-authored-by: Emil Hultcrantz <[email protected]> Co-authored-by: Anna Maartensson <[email protected]> Co-authored-by: Tian0602 <[email protected]> Co-authored-by: LottaJohnsson <[email protected]> Co-authored-by: Filippa Nilsson <[email protected]> Co-authored-by: Filippa Nilsson <[email protected]> Co-authored-by: Siedlerchr <[email protected]> Co-authored-by: Anders Blomqvist <[email protected]> Co-authored-by: ZOU Hetai <[email protected]> Co-authored-by: Hannes Stig <[email protected]> Co-authored-by: Elliot <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roc <[email protected]>
1 parent 295035a commit a1b6d38

File tree

28 files changed

+1017
-343
lines changed

28 files changed

+1017
-343
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
5454
- 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)
5555
- Keywords field are now displayed as tags. [#10910](https://github.com/JabRef/jabref/pull/10910)
5656
- 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)
57+
- 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)
5758

5859
### Fixed
5960

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ dependencies {
252252
// parse plist files
253253
implementation 'com.googlecode.plist:dd-plist:1.28'
254254

255+
// YAML formatting
256+
implementation 'org.yaml:snakeyaml:2.2'
257+
255258
testImplementation 'io.github.classgraph:classgraph:4.8.168'
256259
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
257260
testImplementation 'org.junit.platform:junit-platform-launcher:1.10.2'
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
nav_order: 28
3+
parent: Decision Records
4+
---
5+
6+
<!-- we need to disable MD025, because we use the different heading "ADR Template" in the homepage (see above) than it is foreseen in the template -->
7+
<!-- markdownlint-disable-next-line MD025 -->
8+
# Exporting multiple entries to CFF
9+
10+
## Context and Problem Statement
11+
12+
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.
13+
14+
## Decision Drivers
15+
16+
* Make exported files compatible with official CFF tools
17+
* Make exporting process logical for users
18+
19+
## Considered Options
20+
21+
* When exporting:
22+
* Export non-`software` entries with dummy topmost `sofware` and entries as `preferred-citation`
23+
* Export non-`software` entries with dummy topmost `sofware` and entries as `references`
24+
* Forbid exporting multiple entries at once
25+
* Forbid exporting more than one software entry at once
26+
* Export entries in several files (i.e. one / file)
27+
* Export several `software` entries with one of them topmost and all others as `references`
28+
* Export several `software` entries with a dummy topmost `software` element and all others as `references`
29+
* When importing:
30+
* Only create one entry / file, enven if there is a `preferred-citation` or `references`
31+
* Add a JabRef `cites` relation from `software` entry to its `preferred-citation`
32+
* Add a JabRef `cites` relation from `preferred-citation` entry to the main `software` entry
33+
* Separate `software` entries from their `preferred-citation` or `references`
34+
35+
## Decision Outcome
36+
37+
The decision outcome is the following.
38+
39+
* When exporting, JabRef will have a different behavior depending on entries type.
40+
* If multiple non-`software` entries are selected, then exporter uses the `references` field with a dummy topmost `software` element.
41+
* 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.
42+
* 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.
43+
* JabRef will not handle `cites` or `related` fields for non-`software` elements.
44+
* 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.
45+
46+
### Positive Consequences
47+
48+
* Exported results comply with CFF format
49+
* The export process is "logic" : an user who exports multiple files to CFF might find it clear that they are all marked as `references`
50+
* Importing a CFF file and then exporting the "main" (software) created entry is consistent and will produce the same result
51+
52+
### Negative Consequences
53+
54+
* 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)
55+
* `cites` and `related` fields of non-`software` entries are not supported

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,5 @@
145145
requires de.saxsys.mvvmfx.validation;
146146
requires com.jthemedetector;
147147
requires dd.plist;
148+
requires org.yaml.snakeyaml;
148149
}

src/main/java/org/jabref/cli/ArgumentProcessor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ private Optional<ParserResult> importFile(Path file, String importFormat) {
166166
ImportFormatReader importFormatReader = new ImportFormatReader(
167167
preferencesService.getImporterPreferences(),
168168
preferencesService.getImportFormatPreferences(),
169-
fileUpdateMonitor);
169+
preferencesService.getCitationKeyPatternPreferences(),
170+
fileUpdateMonitor
171+
);
170172

171173
if (!"*".equals(importFormat)) {
172174
System.out.println(Localization.lang("Importing %0", file));

src/main/java/org/jabref/cli/JabRefCLI.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ public static void printUsage(PreferencesService preferencesService) {
317317
ImportFormatReader importFormatReader = new ImportFormatReader(
318318
preferencesService.getImporterPreferences(),
319319
preferencesService.getImportFormatPreferences(),
320-
new DummyFileUpdateMonitor());
320+
preferencesService.getCitationKeyPatternPreferences(),
321+
new DummyFileUpdateMonitor()
322+
);
321323
List<Pair<String, String>> importFormats = importFormatReader
322324
.getImportFormats().stream()
323325
.map(format -> new Pair<>(format.getName(), format.getId()))

src/main/java/org/jabref/gui/externalfiles/ImportHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,9 @@ private List<BibEntry> tryImportFormats(String data) {
372372
ImportFormatReader importFormatReader = new ImportFormatReader(
373373
preferencesService.getImporterPreferences(),
374374
preferencesService.getImportFormatPreferences(),
375-
fileUpdateMonitor);
375+
preferencesService.getCitationKeyPatternPreferences(),
376+
fileUpdateMonitor
377+
);
376378
UnknownFormatImport unknownFormatImport = importFormatReader.importUnknownFormat(data);
377379
return unknownFormatImport.parserResult().getDatabase().getEntries();
378380
} catch (ImportException ex) { // ex is already localized

src/main/java/org/jabref/gui/importer/ImportCommand.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ public void execute() {
7979
ImportFormatReader importFormatReader = new ImportFormatReader(
8080
preferencesService.getImporterPreferences(),
8181
preferencesService.getImportFormatPreferences(),
82-
fileUpdateMonitor);
82+
preferencesService.getCitationKeyPatternPreferences(),
83+
fileUpdateMonitor
84+
);
8385
SortedSet<Importer> importers = importFormatReader.getImportFormats();
8486

8587
FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
@@ -134,7 +136,9 @@ private ParserResult doImport(List<Path> files, Importer importFormat) throws IO
134136
ImportFormatReader importFormatReader = new ImportFormatReader(
135137
preferencesService.getImporterPreferences(),
136138
preferencesService.getImportFormatPreferences(),
137-
fileUpdateMonitor);
139+
preferencesService.getCitationKeyPatternPreferences(),
140+
fileUpdateMonitor
141+
);
138142
for (Path filename : files) {
139143
try {
140144
if (importer.isEmpty()) {
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package org.jabref.logic.exporter;
2+
3+
import java.io.FileWriter;
4+
import java.io.IOException;
5+
import java.nio.charset.StandardCharsets;
6+
import java.nio.file.Path;
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.LinkedHashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Objects;
13+
import java.util.Optional;
14+
15+
import org.jabref.logic.util.StandardFileType;
16+
import org.jabref.model.database.BibDatabaseContext;
17+
import org.jabref.model.entry.Author;
18+
import org.jabref.model.entry.AuthorList;
19+
import org.jabref.model.entry.BibEntry;
20+
import org.jabref.model.entry.Date;
21+
import org.jabref.model.entry.field.BiblatexSoftwareField;
22+
import org.jabref.model.entry.field.Field;
23+
import org.jabref.model.entry.field.StandardField;
24+
import org.jabref.model.entry.field.UnknownField;
25+
import org.jabref.model.entry.types.EntryType;
26+
import org.jabref.model.entry.types.StandardEntryType;
27+
28+
import org.yaml.snakeyaml.DumperOptions;
29+
import org.yaml.snakeyaml.Yaml;
30+
31+
public class CffExporter extends Exporter {
32+
// Fields that are taken 1:1 from BibTeX to CFF
33+
public static final List<String> UNMAPPED_FIELDS = List.of(
34+
"abbreviation", "collection-doi", "collection-title", "collection-type", "commit", "copyright",
35+
"data-type", "database", "date-accessed", "date-downloaded", "date-published", "department", "end",
36+
"entry", "filename", "format", "issue-date", "issue-title", "license-url", "loc-end", "loc-start",
37+
"medium", "nihmsid", "number-volumes", "patent-states", "pmcid", "repository-artifact", "repository-code",
38+
"scope", "section", "start", "term", "thesis-type", "volume-title", "year-original"
39+
);
40+
41+
public static final Map<Field, String> FIELDS_MAP = Map.ofEntries(
42+
Map.entry(StandardField.ABSTRACT, "abstract"),
43+
Map.entry(StandardField.DATE, "date-released"),
44+
Map.entry(StandardField.DOI, "doi"),
45+
Map.entry(StandardField.KEYWORDS, "keywords"),
46+
Map.entry(BiblatexSoftwareField.LICENSE, "license"),
47+
Map.entry(StandardField.COMMENT, "message"),
48+
Map.entry(BiblatexSoftwareField.REPOSITORY, "repository"),
49+
Map.entry(StandardField.TITLE, "title"),
50+
Map.entry(StandardField.URL, "url"),
51+
Map.entry(StandardField.VERSION, "version"),
52+
Map.entry(StandardField.EDITION, "edition"),
53+
Map.entry(StandardField.ISBN, "isbn"),
54+
Map.entry(StandardField.ISSN, "issn"),
55+
Map.entry(StandardField.ISSUE, "issue"),
56+
Map.entry(StandardField.JOURNAL, "journal"),
57+
Map.entry(StandardField.MONTH, "month"),
58+
Map.entry(StandardField.NOTE, "notes"),
59+
Map.entry(StandardField.NUMBER, "number"),
60+
Map.entry(StandardField.PAGES, "pages"),
61+
Map.entry(StandardField.PUBSTATE, "status"),
62+
Map.entry(StandardField.VOLUME, "volume"),
63+
Map.entry(StandardField.YEAR, "year")
64+
);
65+
66+
public static final Map<EntryType, String> TYPES_MAP = Map.ofEntries(
67+
Map.entry(StandardEntryType.Article, "article"),
68+
Map.entry(StandardEntryType.Book, "book"),
69+
Map.entry(StandardEntryType.Booklet, "pamphlet"),
70+
Map.entry(StandardEntryType.Proceedings, "conference"),
71+
Map.entry(StandardEntryType.InProceedings, "conference-paper"),
72+
Map.entry(StandardEntryType.Misc, "misc"),
73+
Map.entry(StandardEntryType.Manual, "manual"),
74+
Map.entry(StandardEntryType.Software, "software"),
75+
Map.entry(StandardEntryType.Dataset, "dataset"),
76+
Map.entry(StandardEntryType.Report, "report"),
77+
Map.entry(StandardEntryType.Unpublished, "unpublished")
78+
);
79+
80+
public CffExporter() {
81+
super("cff", "CFF", StandardFileType.CFF);
82+
}
83+
84+
@Override
85+
public void export(BibDatabaseContext databaseContext, Path file, List<BibEntry> entries) throws Exception {
86+
Objects.requireNonNull(databaseContext);
87+
Objects.requireNonNull(file);
88+
Objects.requireNonNull(entries);
89+
90+
// Do not export if no entries to export -- avoids exports with only template text
91+
if (entries.isEmpty()) {
92+
return;
93+
}
94+
95+
// Make a copy of the list to avoid modifying the original list
96+
final List<BibEntry> entriesToTransform = new ArrayList<>(entries);
97+
98+
// Set up YAML options
99+
DumperOptions options = new DumperOptions();
100+
options.setWidth(Integer.MAX_VALUE);
101+
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
102+
options.setPrettyFlow(true);
103+
options.setIndentWithIndicator(true);
104+
options.setIndicatorIndent(2);
105+
Yaml yaml = new Yaml(options);
106+
107+
BibEntry main = null;
108+
boolean mainIsDummy = false;
109+
int countOfSoftwareAndDataSetEntries = 0;
110+
for (BibEntry entry : entriesToTransform) {
111+
if (entry.getType() == StandardEntryType.Software || entry.getType() == StandardEntryType.Dataset) {
112+
main = entry;
113+
countOfSoftwareAndDataSetEntries++;
114+
}
115+
}
116+
if (countOfSoftwareAndDataSetEntries == 1) {
117+
// If there is only one software or dataset entry, use it as the main entry
118+
entriesToTransform.remove(main);
119+
} else {
120+
// If there are no software or dataset entries, create a dummy main entry holding the given entries
121+
main = new BibEntry(StandardEntryType.Software);
122+
mainIsDummy = true;
123+
}
124+
125+
// Transform main entry to CFF format
126+
Map<String, Object> cffData = transformEntry(main, true, mainIsDummy);
127+
128+
// Preferred citation
129+
if (main.hasField(StandardField.CITES)) {
130+
String citeKey = main.getField(StandardField.CITES).orElse("").split(",")[0];
131+
List<BibEntry> citedEntries = databaseContext.getDatabase().getEntriesByCitationKey(citeKey);
132+
entriesToTransform.removeAll(citedEntries);
133+
if (!citedEntries.isEmpty()) {
134+
BibEntry citedEntry = citedEntries.getFirst();
135+
cffData.put("preferred-citation", transformEntry(citedEntry, false, false));
136+
}
137+
}
138+
139+
// References
140+
List<Map<String, Object>> related = new ArrayList<>();
141+
if (main.hasField(StandardField.RELATED)) {
142+
main.getEntryLinkList(StandardField.RELATED, databaseContext.getDatabase())
143+
.stream()
144+
.map(link -> link.getLinkedEntry())
145+
.filter(Optional::isPresent)
146+
.map(Optional::get)
147+
.forEach(entry -> {
148+
related.add(transformEntry(entry, false, false));
149+
entriesToTransform.remove(entry);
150+
});
151+
}
152+
153+
// Add remaining entries as references
154+
for (BibEntry entry : entriesToTransform) {
155+
related.add(transformEntry(entry, false, false));
156+
}
157+
if (!related.isEmpty()) {
158+
cffData.put("references", related);
159+
}
160+
161+
try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) {
162+
yaml.dump(cffData, writer);
163+
} catch (IOException ex) {
164+
throw new SaveException(ex);
165+
}
166+
}
167+
168+
private Map<String, Object> transformEntry(BibEntry entry, boolean main, boolean dummy) {
169+
Map<String, Object> cffData = new LinkedHashMap<>();
170+
Map<Field, String> fields = new HashMap<>(entry.getFieldMap());
171+
172+
if (main) {
173+
// Mandatory CFF version field
174+
cffData.put("cff-version", "1.2.0");
175+
176+
// Mandatory message field
177+
String message = fields.getOrDefault(StandardField.COMMENT,
178+
"If you use this software, please cite it using the metadata from this file.");
179+
cffData.put("message", message);
180+
fields.remove(StandardField.COMMENT);
181+
}
182+
183+
// Mandatory title field
184+
String title = fields.getOrDefault(StandardField.TITLE, "No title specified.");
185+
cffData.put("title", title);
186+
fields.remove(StandardField.TITLE);
187+
188+
// Mandatory authors field
189+
List<Author> authors = AuthorList.parse(fields.getOrDefault(StandardField.AUTHOR, ""))
190+
.getAuthors();
191+
parseAuthors(cffData, authors);
192+
fields.remove(StandardField.AUTHOR);
193+
194+
// Type
195+
if (!dummy) {
196+
cffData.put("type", TYPES_MAP.getOrDefault(entry.getType(), "misc"));
197+
}
198+
199+
// Keywords
200+
String keywords = fields.getOrDefault(StandardField.KEYWORDS, null);
201+
if (keywords != null) {
202+
cffData.put("keywords", keywords.split(",\\s*"));
203+
}
204+
fields.remove(StandardField.KEYWORDS);
205+
206+
// Date
207+
String date = fields.getOrDefault(StandardField.DATE, null);
208+
if (date != null) {
209+
parseDate(cffData, date);
210+
}
211+
fields.remove(StandardField.DATE);
212+
213+
// Remaining fields not handled above
214+
for (Field field : fields.keySet()) {
215+
if (FIELDS_MAP.containsKey(field)) {
216+
cffData.put(FIELDS_MAP.get(field), fields.get(field));
217+
} else if (field instanceof UnknownField) {
218+
// Check that field is accepted by CFF format specification
219+
if (UNMAPPED_FIELDS.contains(field.getName())) {
220+
cffData.put(field.getName(), fields.get(field));
221+
}
222+
}
223+
}
224+
return cffData;
225+
}
226+
227+
private void parseAuthors(Map<String, Object> data, List<Author> authors) {
228+
List<Map<String, String>> authorsList = new ArrayList<>();
229+
authors.forEach(author -> {
230+
Map<String, String> authorMap = new LinkedHashMap<>();
231+
if (author.getFamilyName().isPresent()) {
232+
authorMap.put("family-names", author.getFamilyName().get());
233+
}
234+
if (author.getGivenName().isPresent()) {
235+
authorMap.put("given-names", author.getGivenName().get());
236+
}
237+
if (author.getNamePrefix().isPresent()) {
238+
authorMap.put("name-particle", author.getNamePrefix().get());
239+
}
240+
if (author.getNameSuffix().isPresent()) {
241+
authorMap.put("name-suffix", author.getNameSuffix().get());
242+
}
243+
authorsList.add(authorMap);
244+
});
245+
data.put("authors", authorsList.isEmpty() ? List.of(Map.of("name", "/")) : authorsList);
246+
}
247+
248+
private void parseDate(Map<String, Object> data, String date) {
249+
Optional<Date> parsedDateOpt = Date.parse(date);
250+
if (parsedDateOpt.isEmpty()) {
251+
data.put("issue-date", date);
252+
return;
253+
}
254+
Date parsedDate = parsedDateOpt.get();
255+
if (parsedDate.getYear().isPresent() && parsedDate.getMonth().isPresent() && parsedDate.getDay().isPresent()) {
256+
data.put("date-released", parsedDate.getNormalized());
257+
return;
258+
}
259+
parsedDate.getMonth().ifPresent(month -> data.put("month", month.getNumber()));
260+
parsedDate.getYear().ifPresent(year -> data.put("year", year));
261+
}
262+
}
263+

0 commit comments

Comments
 (0)