diff --git a/CHANGELOG.md b/CHANGELOG.md index 591970d3715..2a72bfb1098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Added +- We added import support for CFF files. [#7945](https://github.com/JabRef/jabref/issues/7945) - We added the option to copy the DOI of an entry directly from the context menu copy submenu. [#7826](https://github.com/JabRef/jabref/issues/7826) - We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838) - We improved the deduction of bib-entries from imported fulltext pdfs. [#7947](https://github.com/JabRef/jabref/pull/7947) diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index 903d971c4bb..c942f05f92c 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -12,6 +12,7 @@ import org.jabref.logic.importer.fileformat.BibTeXMLImporter; import org.jabref.logic.importer.fileformat.BiblioscapeImporter; import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.importer.fileformat.CffImporter; import org.jabref.logic.importer.fileformat.CopacImporter; import org.jabref.logic.importer.fileformat.EndnoteImporter; import org.jabref.logic.importer.fileformat.EndnoteXmlImporter; @@ -79,6 +80,7 @@ public void resetImportFormats(ImportSettingsPreferences importSettingsPreferenc formats.add(new RepecNepImporter(importFormatPreferences)); formats.add(new RisImporter()); formats.add(new SilverPlatterImporter()); + formats.add(new CffImporter()); formats.add(new BiblioscapeImporter()); formats.add(new BibtexImporter(importFormatPreferences, fileMonitor)); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java new file mode 100644 index 00000000000..c683841ae77 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -0,0 +1,201 @@ +package org.jabref.logic.importer.fileformat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jabref.logic.importer.Importer; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.entry.Author; +import org.jabref.model.entry.AuthorList; +import org.jabref.model.entry.BibEntry; +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.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; + +public class CffImporter extends Importer { + + @Override + public String getName() { + return "CFF"; + } + + @Override + public StandardFileType getFileType() { + return StandardFileType.CFF; + } + + @Override + public String getId() { + return "cff"; + } + + @Override + public String getDescription() { + return "Importer for the CFF format. Is only used to cite software, one entry per file."; + } + + // POJO classes for yaml data + private static class CffFormat { + private final HashMap values = new HashMap<>(); + + @JsonProperty("authors") + private List authors; + + @JsonProperty("identifiers") + private List ids; + + public CffFormat() { + } + + @JsonAnySetter + private void setValues(String key, String value) { + values.put(key, value); + } + } + + private static class CffAuthor { + private final HashMap values = new HashMap<>(); + + public CffAuthor() { + } + + @JsonAnySetter + private void setValues(String key, String value) { + values.put(key, value); + } + + } + + private static class CffIdentifier { + @JsonProperty("type") + private String type; + @JsonProperty("value") + private String value; + + public CffIdentifier() { + } + } + + @Override + public ParserResult importDatabase(BufferedReader reader) throws IOException { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + CffFormat citation = mapper.readValue(reader, CffFormat.class); + 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 (property.getKey().equals("type")) { + if (property.getValue().equals("dataset")) { + entryType = StandardEntryType.Dataset; + } + } else if (getUnmappedFields().contains(property.getKey())) { + entryMap.put(new UnknownField(property.getKey()), property.getValue()); + } + } + + // 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); + + // Select DOI to keep + if (entryMap.get(StandardField.DOI) == null && citation.ids != null) { + List doiIds = citation.ids.stream() + .filter(id -> id.type.equals("doi")) + .collect(Collectors.toList()); + if (doiIds.size() == 1) { + entryMap.put(StandardField.DOI, doiIds.get(0).value); + } + } + + // Select SWHID to keep + if (citation.ids != null) { + List swhIds = citation.ids.stream() + .filter(id -> id.type.equals("swh")) + .map(id -> id.value) + .collect(Collectors.toList()); + + if (swhIds.size() == 1) { + entryMap.put(StandardField.SWHID, swhIds.get(0)); + } else if (swhIds.size() > 1) { + List relSwhIds = swhIds.stream() + .filter(id -> id.split(":").length > 3) // quick filter for invalid swhids + .filter(id -> id.split(":")[2].equals("rel")) + .collect(Collectors.toList()); + if (relSwhIds.size() == 1) { + entryMap.put(StandardField.SWHID, relSwhIds.get(0)); + } + } + } + + BibEntry entry = new BibEntry(entryType); + entry.setField(entryMap); + + List entriesList = new ArrayList<>(); + entriesList.add(entry); + + return new ParserResult(entriesList); + } + + @Override + public boolean isRecognizedFormat(BufferedReader reader) throws IOException { + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + CffFormat citation; + + try { + citation = mapper.readValue(reader, CffFormat.class); + return citation != null && citation.values.get("title") != null; + } catch (IOException e) { + return false; + } + } + + 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", StandardField.LICENSE); + fieldMappings.put("repository", StandardField.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 List getUnmappedFields() { + List fields = new ArrayList<>(); + + fields.add("commit"); + fields.add("license-url"); + fields.add("repository-code"); + fields.add("repository-artifact"); + + return fields; + } +} diff --git a/src/main/java/org/jabref/logic/util/StandardFileType.java b/src/main/java/org/jabref/logic/util/StandardFileType.java index 7a9d5d08c48..17b207a5145 100644 --- a/src/main/java/org/jabref/logic/util/StandardFileType.java +++ b/src/main/java/org/jabref/logic/util/StandardFileType.java @@ -42,6 +42,7 @@ public enum StandardFileType implements FileType { ZIP("Zip Archive", "zip"), CSS("CSS Styleshet", "css"), YAML("YAML Markup", "yaml"), + CFF("CFF", "cff"), ANY_FILE("Any", "*"); private final List extensions; diff --git a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java new file mode 100644 index 00000000000..5dd61a6d262 --- /dev/null +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -0,0 +1,165 @@ +package org.jabref.logic.importer.fileformat; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CffImporterTest { + + private CffImporter importer; + + @BeforeEach + public void setUp() { + importer = new CffImporter(); + } + + @Test + public void testGetFormatName() { + assertEquals("CFF", importer.getName()); + } + + @Test + public void testGetCLIId() { + assertEquals("cff", importer.getId()); + } + + @Test + public void testsGetExtensions() { + assertEquals(StandardFileType.CFF, importer.getFileType()); + } + + @Test + public void testGetDescription() { + assertEquals("Importer for the CFF format. Is only used to cite software, one entry per file.", + importer.getDescription()); + } + + @Test + public void testIsRecognizedFormat() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValid.cff").toURI()); + assertTrue(importer.isRecognizedFormat(file, StandardCharsets.UTF_8)); + } + + @Test + public void testIsRecognizedFormatReject() 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, StandardCharsets.UTF_8)); + } + } + + @Test + public void testImportEntriesBasic() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValid.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry().withField(StandardField.AUTHOR, "Joe van Smith"); + + assertEquals(entry, expected); + } + + @Test + public void testImportEntriesMultipleAuthors() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidMultAuthors.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry(); + + assertEquals(entry, expected); + + } + + @Test + public void testImportEntriesSwhIdSelect1() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidSwhIdSelect1.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry().withField(StandardField.SWHID, "swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f"); + + assertEquals(entry, expected); + } + + @Test + public void testImportEntriesSwhIdSelect2() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestValidSwhIdSelect2.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry().withField(StandardField.SWHID, "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2"); + + assertEquals(entry, expected); + } + + @Test + public void testImportEntriesDataset() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestDataset.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry(); + expected.setType(StandardEntryType.Dataset); + + assertEquals(entry, expected); + } + + @Test + public void testImportEntriesDoiSelect() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestDoiSelect.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry(); + + assertEquals(entry, expected); + } + + @Test + public void testImportEntriesUnknownFields() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestUnknownFields.cff").toURI()); + List bibEntries = importer.importDatabase(file, StandardCharsets.UTF_8).getDatabase().getEntries(); + BibEntry entry = bibEntries.get(0); + + BibEntry expected = getPopulatedEntry().withField(new UnknownField("commit"), "10ad"); + + assertEquals(entry, expected); + } + + 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(StandardField.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(StandardField.LICENSE, "MIT"); + entry.setField(StandardField.VERSION, "1.0"); + + return entry; + } +} diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDataset.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDataset.cff new file mode 100644 index 00000000000..8900971cb80 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDataset.cff @@ -0,0 +1,26 @@ +# 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 +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: dataset +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDoiSelect.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDoiSelect.cff new file mode 100644 index 00000000000..28993253709 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestDoiSelect.cff @@ -0,0 +1,25 @@ +# 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 +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" +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid1.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid1.cff new file mode 100644 index 00000000000..be4a0e39ec4 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid1.cff @@ -0,0 +1,4 @@ +# YAML 1.2 +--- +test: 123 +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid2.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid2.cff new file mode 100644 index 00000000000..c75d438d6aa --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestInvalid2.cff @@ -0,0 +1 @@ +aosdoioifjosdfikbasjc diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestUnknownFields.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestUnknownFields.cff new file mode 100644 index 00000000000..43008aab9dc --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestUnknownFields.cff @@ -0,0 +1,24 @@ +# 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" +commit: 10ad +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValid.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValid.cff new file mode 100644 index 00000000000..472ba55169d --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValid.cff @@ -0,0 +1,19 @@ +# YAML 1.2 +--- +abstract: "Test abstract." +authors: + - + family-names: Smith + given-names: Joe + name-particle: van +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" +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidMultAuthors.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidMultAuthors.cff new file mode 100644 index 00000000000..f69962c1760 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidMultAuthors.cff @@ -0,0 +1,23 @@ +# 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" +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect1.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect1.cff new file mode 100644 index 00000000000..2944a7a7ef2 --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect1.cff @@ -0,0 +1,29 @@ +# 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: + - + type: swh + value: "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2" + - + type: swh + value: "swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f" +license: MIT +message: "Test entry." +title: Test +version: "1.0" +url: "www.google.com" +repository: "www.github.com" +... diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect2.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect2.cff new file mode 100644 index 00000000000..eaa2e655c2a --- /dev/null +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestValidSwhIdSelect2.cff @@ -0,0 +1,26 @@ +# 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: + - + type: swh + value: "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2" +license: MIT +message: "Test entry." +title: Test +version: "1.0" +url: "www.google.com" +repository: "www.github.com" +...