diff --git a/CHANGELOG.md b/CHANGELOG.md index 877eeb600dd..8f3fae0d90d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,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 filed 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 1165b0e8d6b..55aeaa92e4b 100644 --- a/build.gradle +++ b/build.gradle @@ -248,6 +248,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..7b097816db7 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,16 @@ 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, + CitationKeyPatternPreferences citationKeyPatternPreferences, FileUpdateMonitor fileUpdateMonitor) { this.importerPreferences = importerPreferences; this.importFormatPreferences = importFormatPreferences; this.fileUpdateMonitor = fileUpdateMonitor; + this.citationKeyPatternPreferences = citationKeyPatternPreferences; reset(); } @@ -82,7 +86,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 87bb06bc7e4..49e07c9a9d4 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" +... +