From f437d58e96e68ed0cf95ee48218eae7180e8b381 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Fri, 8 Mar 2024 12:37:00 +0100 Subject: [PATCH 01/38] issue #10993 - feat: added ability to parse preferred-citation field to CffImporter --- .../importer/fileformat/CffImporter.java | 103 ++++++++++++++---- .../jabref/logic/layout/format/CffType.java | 1 + .../importer/fileformat/CffImporterTest.java | 22 +++- .../CffImporterPreferredCitation.cff | 37 +++++++ 4 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterPreferredCitation.cff 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..e23335172a2 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -18,6 +18,7 @@ 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; @@ -44,7 +45,8 @@ 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. Is only used to cite software, one entry per file. Can also " + + "cite a preferred citation."; } // POJO classes for yaml data @@ -57,6 +59,9 @@ private static class CffFormat { @JsonProperty("identifiers") private List ids; + @JsonProperty("preferred-citation") + private CffPreferredCitation citation; + public CffFormat() { } @@ -88,43 +93,55 @@ public CffIdentifier() { } } + private static class CffPreferredCitation { + private final HashMap values = new HashMap<>(); + + @JsonProperty("type") + private String type; + + @JsonProperty("authors") + private List authors; + + public CffPreferredCitation() { + } + + @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<>(); + + // Retrieve mappings from CFF to JabRef + HashMap fieldMap = getFieldMappings(); + HashMap typeMap = getTypeMappings(); + + // Parse main entry HashMap entryMap = new HashMap<>(); - StandardEntryType entryType = StandardEntryType.Software; + EntryType entryType = typeMap.getOrDefault(citation.values.get("type"), 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()); } } // 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)); // 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 +152,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 +160,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,10 +169,24 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { BibEntry entry = new BibEntry(entryType); entry.setField(entryMap); - - List entriesList = new ArrayList<>(); entriesList.add(entry); + // Handle `preferred-citation` field + if (citation.citation != null) { + HashMap preferredEntryMap = new HashMap<>(); + EntryType preferredEntryType = typeMap.getOrDefault(citation.citation.type, StandardEntryType.Article); + for (Map.Entry property : citation.citation.values.entrySet()) { + if (fieldMap.containsKey(property.getKey())) { + preferredEntryMap.put(fieldMap.get(property.getKey()), property.getValue()); + } + } + + preferredEntryMap.put(StandardField.AUTHOR, parseAuthors(citation.citation.authors)); + BibEntry preferredEntry = new BibEntry(preferredEntryType); + preferredEntry.setField(preferredEntryMap); + entriesList.add(preferredEntry); + } + return new ParserResult(entriesList); } @@ -185,9 +216,28 @@ private HashMap getFieldMappings() { fieldMappings.put("message", StandardField.COMMENT); fieldMappings.put("date-released", StandardField.DATE); fieldMappings.put("keywords", StandardField.KEYWORDS); + + // specific to preferred-citation + fieldMappings.put("month", StandardField.MONTH); + fieldMappings.put("year", StandardField.YEAR); return fieldMappings; } + private HashMap getTypeMappings() { + HashMap typeMappings = new HashMap<>(); + typeMappings.put("article", StandardEntryType.Article); + typeMappings.put("book", StandardEntryType.Book); + typeMappings.put("pamphlet", StandardEntryType.Booklet); + typeMappings.put("conference-paper", StandardEntryType.InProceedings); + typeMappings.put("misc", StandardEntryType.Misc); + typeMappings.put("manual", StandardEntryType.Manual); + typeMappings.put("software", StandardEntryType.Software); + typeMappings.put("dataset", StandardEntryType.Dataset); + typeMappings.put("report", StandardEntryType.Report); + typeMappings.put("unpublished", StandardEntryType.Unpublished); + return typeMappings; + } + private List getUnmappedFields() { List fields = new ArrayList<>(); @@ -198,4 +248,15 @@ private List getUnmappedFields() { return fields; } + + 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(); + } } diff --git a/src/main/java/org/jabref/logic/layout/format/CffType.java b/src/main/java/org/jabref/logic/layout/format/CffType.java index 5de168b77ba..1e6867136dd 100644 --- a/src/main/java/org/jabref/logic/layout/format/CffType.java +++ b/src/main/java/org/jabref/logic/layout/format/CffType.java @@ -15,6 +15,7 @@ public String format(String value) { case Misc -> "misc"; case Manual -> "manual"; case Software -> "software"; + case Dataset -> "dataset"; case Report, TechReport -> "report"; case Unpublished -> "unpublished"; default -> "generic"; 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..b9462fbae9a 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -46,7 +46,8 @@ 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. Is only used to cite software, one entry per file. " + + "Can also cite a preferred citation.", importer.getDescription()); } @@ -144,10 +145,27 @@ public void importEntriesUnknownFields() throws IOException, URISyntaxException 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(); + + BibEntry expectedMain = getPopulatedEntry(); + BibEntry expectedPreferred = new BibEntry(StandardEntryType.InProceedings); + expectedPreferred.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); + expectedPreferred.setField(StandardField.DOI, "10.0001/TEST"); + expectedPreferred.setField(StandardField.URL, "www.github.com"); + + assertEquals(mainEntry, expectedMain); + assertEquals(preferredEntry, expectedPreferred); + } + 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"); 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" + +... From b5298dff306f4a61c86ae92f7b12955d142f3e78 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Fri, 8 Mar 2024 15:27:17 +0100 Subject: [PATCH 02/38] issue #10993 - feat: added all fields of JabRef/CITATION.cff to CffImporter --- .../org/jabref/logic/importer/fileformat/CffImporter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 e23335172a2..814a938b681 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -6,7 +6,6 @@ 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; @@ -220,6 +219,10 @@ private HashMap getFieldMappings() { // specific to preferred-citation fieldMappings.put("month", StandardField.MONTH); fieldMappings.put("year", StandardField.YEAR); + fieldMappings.put("journal", StandardField.JOURNAL); + fieldMappings.put("issue", StandardField.ISSUE); + fieldMappings.put("volume", StandardField.VOLUME); + fieldMappings.put("number", StandardField.NUMBER); return fieldMappings; } From a6b62e10f39f0d1d501c4e0b94f63cac97c9d845 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 11 Mar 2024 11:35:33 +0100 Subject: [PATCH 03/38] issue #10993 - feat: rewrote CffExporter to parse Software, Dataset types and authors names correctly --- .../jabref/logic/exporter/CffExporter.java | 223 ++++++++++++++++++ .../logic/exporter/ExporterFactory.java | 2 +- .../org/jabref/logic/layout/LayoutEntry.java | 4 - .../jabref/logic/layout/format/CffDate.java | 70 ------ .../jabref/logic/layout/format/CffType.java | 25 -- src/main/resources/resource/layout/cff.layout | 17 -- .../logic/exporter/CffExporterTest.java | 169 +++++++++---- .../logic/layout/format/CffDateTest.java | 45 ---- 8 files changed, 348 insertions(+), 207 deletions(-) create mode 100644 src/main/java/org/jabref/logic/exporter/CffExporter.java delete mode 100644 src/main/java/org/jabref/logic/layout/format/CffDate.java delete mode 100644 src/main/java/org/jabref/logic/layout/format/CffType.java delete mode 100644 src/main/resources/resource/layout/cff.layout delete mode 100644 src/test/java/org/jabref/logic/layout/format/CffDateTest.java 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..0cc6cd9303b --- /dev/null +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -0,0 +1,223 @@ +package org.jabref.logic.exporter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.Year; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jabref.logic.util.OS; +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.field.BiblatexSoftwareField; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +/** + * Exporter for exporting in CFF format. + */ +class CffExporter extends Exporter { + 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); + + if (entries.isEmpty()) { // Do not export if no entries to export -- avoids exports with only template text + return; + } + + try (AtomicFileWriter ps = new AtomicFileWriter(file, StandardCharsets.UTF_8)) { + ps.write("# YAML 1.2" + OS.NEWLINE); + ps.write("--" + OS.NEWLINE); + ps.write("cff-version: 1.2.0" + OS.NEWLINE); + ps.write("message: If you use this software, please cite it using the metadata from this file."); + ps.write(OS.NEWLINE); + + for (BibEntry entry : entries) { + // Retrieve all fields + Map entryMap = entry.getFieldMap(); + + // Compulsory title field + String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); + ps.write("title: " + title + OS.NEWLINE); + entryMap.remove(StandardField.TITLE); + + // Compulsory authors field + List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) + .getAuthors(); + writeAuthors(ps, authors, false); + entryMap.remove(StandardField.AUTHOR); + + // Type + Map typeMap = getTypeMappings(); + EntryType entryType = entry.getType(); + boolean pref = false; + switch (entryType) { + case StandardEntryType.Software -> + ps.write("type: software"); + case StandardEntryType.Dataset -> + ps.write("type: dataset"); + default -> { + if (typeMap.containsKey(entryType)) { + pref = true; + ps.write("preferred-citation:" + OS.NEWLINE); + ps.write(" type: " + typeMap.get(entryType) + OS.NEWLINE); + writeAuthors(ps, authors, pref); + ps.write(" title: " + title); + } + } + } + ps.write(OS.NEWLINE); + + // Date + String date = entryMap.getOrDefault(StandardField.DATE, null); + if (date != null) { + writeDate(ps, date, pref); + } + entryMap.remove(StandardField.DATE); + + // Fields + Map fieldMap = getFieldMappings(); + for (Field field : entryMap.keySet()) { + if (fieldMap.containsKey(field)) { + ps.write(pref ? " " : ""); + ps.write(fieldMap.get(field) + ": " + entryMap.get(field) + OS.NEWLINE); + } + } + } + } catch (IOException ex) { + throw new SaveException(ex); + } + } + + private void writeAuthors(AtomicFileWriter ps, List authors, boolean pref) throws Exception { + try { + ps.write(pref ? " " : ""); + ps.write("authors:"); + if (authors.isEmpty()) { + ps.write(pref ? " " : ""); + ps.write(" No author specified."); + } else { + ps.write(OS.NEWLINE); + } + for (Author author : authors) { + boolean hyphen = false; + if (author.getLast().isPresent()) { + ps.write(pref ? " " : ""); + ps.write(" - family-names: " + author.getLast().get() + OS.NEWLINE); + hyphen = true; + } + if (author.getFirst().isPresent()) { + ps.write(pref ? " " : ""); + ps.write(hyphen ? " " : " - "); + ps.write("given-names: " + author.getFirst().get() + OS.NEWLINE); + hyphen = true; + } + if (author.getVon().isPresent()) { + ps.write(pref ? " " : ""); + ps.write(hyphen ? " " : " - "); + ps.write("name-particle: " + author.getVon().get() + OS.NEWLINE); + hyphen = true; + } + if (author.getJr().isPresent()) { + ps.write(pref ? " " : ""); + ps.write(hyphen ? " " : " - "); + ps.write("name-suffix: " + author.getJr().get() + OS.NEWLINE); + } + } + } catch (IOException ex) { + throw new SaveException(ex); + } + } + + private void writeDate(AtomicFileWriter ps, String dateField, boolean pref) throws Exception { + StringBuilder builder = new StringBuilder(); + String formatString = "yyyy-MM-dd"; + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + LocalDate date = LocalDate.parse(dateField, DateTimeFormatter.ISO_LOCAL_DATE); + builder.append(pref ? " " : "").append("date-released: ").append(date.format(formatter)); + } catch (DateTimeParseException e) { + if (pref) { + try { + formatString = "yyyy-MM"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + YearMonth yearMonth = YearMonth.parse(dateField, formatter); + int month = yearMonth.getMonth().getValue(); + int year = yearMonth.getYear(); + builder.append(" month: ").append(month).append(OS.NEWLINE); + builder.append(" year: ").append(year).append(OS.NEWLINE); + } catch (DateTimeParseException f) { + try { + formatString = "yyyy"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + int year = Year.parse(dateField, formatter).getValue(); + builder.append(" year: ").append(year).append(OS.NEWLINE); + } catch (DateTimeParseException g) { + builder.append(" issue-date: ").append(dateField).append(OS.NEWLINE); + } + } + } + } + try { + ps.write(builder.toString()); + } catch (IOException ex) { + throw new SaveException(ex); + } + } + + private Map getTypeMappings() { + Map typeMappings = new HashMap<>(); + typeMappings.put(StandardEntryType.Article, "article"); + typeMappings.put(StandardEntryType.Conference, "article"); + typeMappings.put(StandardEntryType.Book, "book"); + typeMappings.put(StandardEntryType.Booklet, "pamphlet"); + typeMappings.put(StandardEntryType.InProceedings, "conference-paper"); + typeMappings.put(StandardEntryType.Proceedings, "proceedings"); + typeMappings.put(StandardEntryType.Misc, "misc"); + typeMappings.put(StandardEntryType.Manual, "manual"); + typeMappings.put(StandardEntryType.Report, "report"); + typeMappings.put(StandardEntryType.TechReport, "report"); + typeMappings.put(StandardEntryType.Unpublished, "unpublished"); + return typeMappings; + } + + private Map getFieldMappings() { + Map fieldMappings = new HashMap<>(); + fieldMappings.put(StandardField.TITLE, "title"); + fieldMappings.put(StandardField.VERSION, "version"); + fieldMappings.put(StandardField.DOI, "doi"); + fieldMappings.put(BiblatexSoftwareField.LICENSE, "license"); + fieldMappings.put(BiblatexSoftwareField.REPOSITORY, "repository"); + fieldMappings.put(StandardField.URL, "url"); + fieldMappings.put(StandardField.ABSTRACT, "abstract"); + fieldMappings.put(StandardField.COMMENT, "message"); + fieldMappings.put(StandardField.DATE, "date-released"); + fieldMappings.put(StandardField.KEYWORDS, "keywords"); + fieldMappings.put(StandardField.MONTH, "month"); + fieldMappings.put(StandardField.YEAR, "year"); + fieldMappings.put(StandardField.JOURNAL, "journal"); + fieldMappings.put(StandardField.ISSUE, "issue"); + fieldMappings.put(StandardField.VOLUME, "volume"); + fieldMappings.put(StandardField.NUMBER, "number"); + return fieldMappings; + } +} + 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/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 1e6867136dd..00000000000 --- a/src/main/java/org/jabref/logic/layout/format/CffType.java +++ /dev/null @@ -1,25 +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 Dataset -> "dataset"; - case Report, TechReport -> "report"; - case Unpublished -> "unpublished"; - default -> "generic"; - }; - } -} - 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..d40061e3b52 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -1,3 +1,4 @@ + package org.jabref.logic.exporter; import java.nio.file.Files; @@ -5,21 +6,16 @@ import java.util.Collections; import java.util.List; -import org.jabref.logic.layout.LayoutFormatterPreferences; -import org.jabref.logic.util.StandardFileType; 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; import org.junit.jupiter.api.io.TempDir; -import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; public class CffExporterTest { @@ -28,16 +24,7 @@ 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(); } @@ -62,17 +49,21 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception 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\""); + "# YAML 1.2", + "--", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: Test Title", + "authors:", + " - family-names: Author", + " given-names: Test", + "preferred-citation:", + " type: article", + " authors:", + " - family-names: Author", + " given-names: Test", + " title: Test Title", + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } @@ -80,7 +71,6 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception @Test public final void usesCorrectType(@TempDir Path tempFile) 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"); @@ -90,15 +80,19 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( + "# YAML 1.2", + "--", "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", - "authors:", - " - name: Test Author", + "message: If you use this software, please cite it using the metadata from this file.", "title: Test Title", + "authors:", + " - family-names: Author", + " given-names: Test", "preferred-citation:", " type: conference-paper", " authors:", - " - name: Test Author", + " - family-names: Author", + " given-names: Test", " title: Test Title", " doi: random_doi_value"); @@ -107,30 +101,110 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { @Test public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Exception { - BibEntry entry = new BibEntry(StandardEntryType.Thesis) - .withCitationKey("test"); + BibEntry entry = new BibEntry(StandardEntryType.Thesis); Path file = tempFile.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( + "# YAML 1.2", + "--", "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", - "authors:", - " - name: No author specified.", + "message: If you use this software, please cite it using the metadata from this file.", "title: No title specified.", + "authors: No author specified." + ); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void exportsSoftwareCorrectly(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Software) + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DOI, "random_doi_value"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "# YAML 1.2", + "--", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: Test Title", + "authors:", + " - family-names: Author", + " given-names: Test", + "type: software", + "doi: random_doi_value"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void exportsSoftwareDateCorrectly(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Software) + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DATE, "2003-11-06"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "# YAML 1.2", + "--", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: Test Title", + "authors:", + " - 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 tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DATE, "2003-11"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "# YAML 1.2", + "--", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: Test Title", + "authors:", + " - family-names: Author", + " given-names: Test", "preferred-citation:", - " type: generic", + " type: article", " authors:", - " - name: No author specified.", - " title: No title specified."); + " - family-names: Author", + " given-names: Test", + " title: Test Title", + " month: 11", + " year: 2003"); assertEquals(expected, Files.readAllLines(file)); } @Test - void passesModifiedCharset(@TempDir Path tempFile) throws Exception { + public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception { BibEntry entry = new BibEntry(StandardEntryType.Article) .withCitationKey("test") .withField(StandardField.AUTHOR, "谷崎 潤一郎") @@ -142,18 +216,23 @@ void passesModifiedCharset(@TempDir Path tempFile) throws Exception { cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( + "# YAML 1.2", + "--", "cff-version: 1.2.0", - "message: \"If you use this, please cite the work from preferred-citation.\"", - "authors:", - " - name: 谷崎 潤一郎", + "message: If you use this software, please cite it using the metadata from this file.", "title: 細雪", + "authors:", + " - family-names: 潤一郎", + " given-names: 谷崎", "preferred-citation:", " type: article", " authors:", - " - name: 谷崎 潤一郎", + " - family-names: 潤一郎", + " given-names: 谷崎", " title: 細雪", - " url: \"http://example.com\""); + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } } + 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")); - } -} From cd94d33bd3b342074a67fef21708d919f18ba325 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 11 Mar 2024 15:41:40 +0100 Subject: [PATCH 04/38] issue #10993 - feat: added keywords and unknown fields support --- .../jabref/logic/exporter/CffExporter.java | 35 +++++++++++---- .../importer/fileformat/CffImporter.java | 9 ++++ .../logic/exporter/CffExporterTest.java | 44 +++++++++---------- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 0cc6cd9303b..37e299544a2 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -22,6 +22,7 @@ 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; @@ -45,18 +46,22 @@ public void export(BibDatabaseContext databaseContext, Path file, List try (AtomicFileWriter ps = new AtomicFileWriter(file, StandardCharsets.UTF_8)) { ps.write("# YAML 1.2" + OS.NEWLINE); - ps.write("--" + OS.NEWLINE); + ps.write("---" + OS.NEWLINE); ps.write("cff-version: 1.2.0" + OS.NEWLINE); - ps.write("message: If you use this software, please cite it using the metadata from this file."); - ps.write(OS.NEWLINE); for (BibEntry entry : entries) { // Retrieve all fields Map entryMap = entry.getFieldMap(); + // Compulsory message field + String message = entryMap.getOrDefault(StandardField.COMMENT, + "If you use this software, please cite it using the metadata from this file."); + ps.write("message: " + message + OS.NEWLINE); + entryMap.remove(StandardField.COMMENT); + // Compulsory title field String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); - ps.write("title: " + title + OS.NEWLINE); + ps.write("title: " + "\"" + title + "\"" + OS.NEWLINE); entryMap.remove(StandardField.TITLE); // Compulsory authors field @@ -79,13 +84,25 @@ public void export(BibDatabaseContext databaseContext, Path file, List pref = true; ps.write("preferred-citation:" + OS.NEWLINE); ps.write(" type: " + typeMap.get(entryType) + OS.NEWLINE); - writeAuthors(ps, authors, pref); - ps.write(" title: " + title); + writeAuthors(ps, authors, true); + ps.write(" title: " + "\"" + title + "\""); } } } ps.write(OS.NEWLINE); + // Keywords + String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); + if (keywords != null) { + ps.write(pref ? " " : ""); + ps.write("keywords:" + OS.NEWLINE); + for (String keyword : keywords.split(",\\s*")) { + ps.write(pref ? " " : ""); + ps.write(" - " + keyword + OS.NEWLINE); + } + } + entryMap.remove(StandardField.KEYWORDS); + // Date String date = entryMap.getOrDefault(StandardField.DATE, null); if (date != null) { @@ -96,9 +113,11 @@ public void export(BibDatabaseContext databaseContext, Path file, List // Fields Map fieldMap = getFieldMappings(); for (Field field : entryMap.keySet()) { + ps.write(pref ? " " : ""); if (fieldMap.containsKey(field)) { - ps.write(pref ? " " : ""); - ps.write(fieldMap.get(field) + ": " + entryMap.get(field) + OS.NEWLINE); + ps.write(fieldMap.get(field) + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); + } else if (field instanceof UnknownField) { + ps.write(field.getName() + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); } } } 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 814a938b681..d8a36581101 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -6,6 +6,7 @@ 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; @@ -61,6 +62,9 @@ private static class CffFormat { @JsonProperty("preferred-citation") private CffPreferredCitation citation; + @JsonProperty("keywords") + private List keywords; + public CffFormat() { } @@ -133,6 +137,11 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { } } + // Parse keywords + if (citation.keywords != null) { + entryMap.put(StandardField.KEYWORDS, String.join(", ", citation.keywords)); + } + // Translate CFF author format to JabRef author format entryMap.put(StandardField.AUTHOR, parseAuthors(citation.authors)); diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index d40061e3b52..07931a5080b 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -50,10 +50,10 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: Test Title", + "title: \"Test Title\"", "authors:", " - family-names: Author", " given-names: Test", @@ -62,8 +62,8 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception " authors:", " - family-names: Author", " given-names: Test", - " title: Test Title", - " url: http://example.com"); + " title: \"Test Title\"", + " url: \"http://example.com\""); assertEquals(expected, Files.readAllLines(file)); } @@ -81,10 +81,10 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: Test Title", + "title: \"Test Title\"", "authors:", " - family-names: Author", " given-names: Test", @@ -93,8 +93,8 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { " authors:", " - family-names: Author", " given-names: Test", - " title: Test Title", - " doi: random_doi_value"); + " title: \"Test Title\"", + " doi: \"random_doi_value\""); assertEquals(expected, Files.readAllLines(file)); } @@ -109,10 +109,10 @@ public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Except List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: No title specified.", + "title: \"No title specified.\"", "authors: No author specified." ); @@ -132,15 +132,15 @@ public final void exportsSoftwareCorrectly(@TempDir Path tempFile) throws Except List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: Test Title", + "title: \"Test Title\"", "authors:", " - family-names: Author", " given-names: Test", "type: software", - "doi: random_doi_value"); + "doi: \"random_doi_value\""); assertEquals(expected, Files.readAllLines(file)); } @@ -158,10 +158,10 @@ public final void exportsSoftwareDateCorrectly(@TempDir Path tempFile) throws Ex List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: Test Title", + "title: \"Test Title\"", "authors:", " - family-names: Author", " given-names: Test", @@ -184,10 +184,10 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: Test Title", + "title: \"Test Title\"", "authors:", " - family-names: Author", " given-names: Test", @@ -196,7 +196,7 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc " authors:", " - family-names: Author", " given-names: Test", - " title: Test Title", + " title: \"Test Title\"", " month: 11", " year: 2003"); @@ -217,10 +217,10 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception List expected = List.of( "# YAML 1.2", - "--", + "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: 細雪", + "title: \"細雪\"", "authors:", " - family-names: 潤一郎", " given-names: 谷崎", @@ -229,8 +229,8 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception " authors:", " - family-names: 潤一郎", " given-names: 谷崎", - " title: 細雪", - " url: http://example.com"); + " title: \"細雪\"", + " url: \"http://example.com\""); assertEquals(expected, Files.readAllLines(file)); } From ca0f887032c9d5f8ebae20b799da7d949d4ef295 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 11 Mar 2024 16:51:38 +0100 Subject: [PATCH 05/38] issue #10993 - feat: added round-trip test --- .../jabref/logic/exporter/CffExporter.java | 10 +- .../importer/fileformat/CffImporter.java | 1 - .../logic/exporter/CffExporterTest.java | 98 +++++++++++++++++++ .../logic/importer/fileformat/CITATION.cff | 58 +++++++++++ 4 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CITATION.cff diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 37e299544a2..cf2d7246102 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -76,20 +76,19 @@ public void export(BibDatabaseContext databaseContext, Path file, List boolean pref = false; switch (entryType) { case StandardEntryType.Software -> - ps.write("type: software"); + ps.write("type: software" + OS.NEWLINE); case StandardEntryType.Dataset -> - ps.write("type: dataset"); + ps.write("type: dataset" + OS.NEWLINE); default -> { if (typeMap.containsKey(entryType)) { pref = true; ps.write("preferred-citation:" + OS.NEWLINE); ps.write(" type: " + typeMap.get(entryType) + OS.NEWLINE); writeAuthors(ps, authors, true); - ps.write(" title: " + "\"" + title + "\""); + ps.write(" title: " + "\"" + title + "\"" + OS.NEWLINE); } } } - ps.write(OS.NEWLINE); // Keywords String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); @@ -113,10 +112,11 @@ public void export(BibDatabaseContext databaseContext, Path file, List // Fields Map fieldMap = getFieldMappings(); for (Field field : entryMap.keySet()) { - ps.write(pref ? " " : ""); if (fieldMap.containsKey(field)) { + ps.write(pref ? " " : ""); ps.write(fieldMap.get(field) + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); } else if (field instanceof UnknownField) { + ps.write(pref ? " " : ""); ps.write(field.getName() + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); } } 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 d8a36581101..13e4680e96e 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -6,7 +6,6 @@ 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; diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index 07931a5080b..f4f8ffd6c71 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -5,7 +5,12 @@ 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.importer.fileformat.CffImporter; +import org.jabref.logic.importer.fileformat.CffImporterTest; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; @@ -234,5 +239,98 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception assertEquals(expected, Files.readAllLines(file)); } + + @Test + public final void roundTripTest(@TempDir Path tempDir) throws Exception { + + // First, import the file which will be parsed as two entries + CffImporter importer = new CffImporter(); + Path file = Path.of(CffImporterTest.class.getResource("CITATION.cff").toURI()); + List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); + BibEntry softwareEntry = bibEntries.getFirst(); + BibEntry articleEntry = bibEntries.getLast(); + + // Then, export them separately and check they have all required fields + Path softwareFile = tempDir.resolve("CITATION_SOFTWARE.cff"); + Path articleFile = tempDir.resolve("CITATION_ARTICLE.cff"); + Files.createFile(softwareFile); + Files.createFile(articleFile); + + cffExporter.export(databaseContext, softwareFile, Collections.singletonList(softwareEntry)); + cffExporter.export(databaseContext, articleFile, Collections.singletonList(articleEntry)); + + Set expectedSoftware = Set.of( + "# YAML 1.2", + "---", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: \"JabRef\"", + "authors:", + " - 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\""); + + Set expectedArticle = Set.of( + "# YAML 1.2", + "---", + "cff-version: 1.2.0", + "message: If you use this software, please cite it using the metadata from this file.", + "title: \"JabRef: BibTeX-based literature management software\"", + "authors:", + " - family-names: Kopp", + " given-names: Oliver", + " - family-names: Snethlage", + " given-names: Carl Christian", + " - family-names: Schwentker", + " given-names: Christoph", + "preferred-citation:", + " type: article", + " authors:", + " - family-names: Kopp", + " given-names: Oliver", + " - family-names: Snethlage", + " given-names: Carl Christian", + " - family-names: Schwentker", + " given-names: Christoph", + " title: \"JabRef: BibTeX-based literature management software\"", + " month: \"11\"", + " issue: \"138\"", + " volume: \"44\"", + " year: \"2023\"", + " doi: \"10.47397/tb/44-3/tb138kopp-jabref\"", + " journal: \"TUGboat\"", + " number: \"3\""); + + // Tests equality of sets since last lines order is random and relies on entries internal order + try (Stream st = Files.lines(softwareFile)) { + assertEquals(expectedSoftware, st.collect(Collectors.toSet())); + } + + try (Stream st = Files.lines(articleFile)) { + assertEquals(expectedArticle, st.collect(Collectors.toSet())); + } + } } 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 From 0b1b578d33ebab0ac1db8a6f6759388042c2e457 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 11 Mar 2024 16:57:55 +0100 Subject: [PATCH 06/38] issue #10993 - doc: updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea79b7a3dc9..ff1b74e1989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We enhanced the dialog for adding new fields in the content selector with a selection box containing a list of standard fields. [#10912](https://github.com/JabRef/jabref/pull/10912) - 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) +- We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) ### Fixed From 56bf7e7a2a05f455c1e469525482875c3dc5d2aa Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Fri, 15 Mar 2024 16:28:23 +0100 Subject: [PATCH 07/38] Convert RemoveBracesFormatterTest to @ParameterizedTest (#11033) * Convert to @ParameterizedTest * Convert to csvsource --------- Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> --- .../RemoveBracesFormatterTest.java | 83 +++++-------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/src/test/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatterTest.java b/src/test/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatterTest.java index 45a418094bc..37eff160e67 100644 --- a/src/test/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatterTest.java +++ b/src/test/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatterTest.java @@ -1,7 +1,8 @@ package org.jabref.logic.formatter.bibtexfields; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -10,67 +11,25 @@ */ public class RemoveBracesFormatterTest { - private RemoveBracesFormatter formatter; - - @BeforeEach - public void setUp() { - formatter = new RemoveBracesFormatter(); - } - - @Test - public void formatRemovesSingleEnclosingBraces() { - assertEquals("test", formatter.format("{test}")); - } - - @Test - public void formatKeepsUnmatchedBracesAtBeginning() { - assertEquals("{test", formatter.format("{test")); - } - - @Test - public void formatKeepsUnmatchedBracesAtEnd() { - assertEquals("test}", formatter.format("test}")); - } - - @Test - public void formatKeepsShortString() { - assertEquals("t", formatter.format("t")); - } - - @Test - public void formatRemovesBracesOnly() { - assertEquals("", formatter.format("{}")); - } - - @Test - public void formatKeepsEmptyString() { - assertEquals("", formatter.format("")); - } - - @Test - public void formatRemovesDoubleEnclosingBraces() { - assertEquals("test", formatter.format("{{test}}")); - } - - @Test - public void formatRemovesTripleEnclosingBraces() { - assertEquals("test", formatter.format("{{{test}}}")); - } - - @Test - public void formatKeepsNonMatchingBraces() { - assertEquals("{A} and {B}", formatter.format("{A} and {B}")); - } - - @Test - public void formatRemovesOnlyMatchingBraces() { - assertEquals("{A} and {B}", formatter.format("{{A} and {B}}")); - } - - @Test - public void formatDoesNotRemoveBracesInBrokenString() { - // We opt here for a conservative approach although one could argue that "A} and {B}" is also a valid return - assertEquals("{A} and {B}}", formatter.format("{A} and {B}}")); + private final RemoveBracesFormatter formatter = new RemoveBracesFormatter(); + + @ParameterizedTest + @CsvSource({ + "test, {test}", // formatRemovesSingleEnclosingBraces + "{test, {test", // formatKeepsUnmatchedBracesAtBeginning + "test}, test}", // formatKeepsUnmatchedBracesAtEnd + "t, t", // formatKeepsShortString + "'', {}", // formatRemovesBracesOnly + "test, {{test}}", // formatKeepsEmptyString + "test, {{{test}}}", // formatRemovesDoubleEnclosingBraces + "{A} and {B}, {A} and {B}", // formatRemovesTripleEnclosingBraces + "{A} and {B}, {{A} and {B}}", // formatKeepsNonMatchingBraces + "{A} and {B}}, {A} and {B}}", // formatRemovesOnlyMatchingBraces + "Vall{\\'e}e Poussin, {Vall{\\'e}e Poussin}", // formatDoesNotRemoveBracesInBrokenString + "Vall{\\'e}e Poussin, Vall{\\'e}e Poussin" + }) + public void format(String expected, String input) { + assertEquals(expected, formatter.format(input)); } @Test From c4b2328a39f30f1577fd4672dd46dd09697d5df7 Mon Sep 17 00:00:00 2001 From: Emil Hultcrantz <90456354+Frequinzy@users.noreply.github.com> Date: Sun, 17 Mar 2024 18:53:12 +0100 Subject: [PATCH 08/38] Importing of BibDesk Groups and Linked Files (#10968) * Add test to check parsing of BibDesk Static Groups * Add test to check parsing of BibDesk Static Groups * Change isExpanded attribute to false in expected groups * remove extra blank line * Add tests to check parsing of BibDesk Smart and mixed groups * Add parsing of BibDesk Files * Attempts at plist * Now parses bdsk-file and shows it as a file in JabRef * Add test for parsing a bdsk-file field * Fix formatting * Add dd-plist library to documentation --------- Co-authored-by: Tian0602 <646432316@qq.com> * Add creation of static JabRef group from a BibDesk file * Creates an empty ExplicitGroup from BibDesk comment * Adds citations to new groups modifies group creations to support multiple groups in the same BibDeskFile * Fix requested changes Refactor imports since they did not match with main Add safety check in addBibDeskGroupEntriesToJabRefGroups --------- Co-authored-by: Filippa Nilsson * Refactor newline to match main branch Co-authored-by: Filippa Nilsson * Add changes to CHANGELOG.md * Reformat indentation to match previous * Revert external libraries Adjust groups serializing * checkstyle and optional magic * fix * fix tests * fix * fix dangling do * better group tree metadata setting * merge group trees, prevent duplicate group assignment in entry Add new BibDesk group Fix IOB for change listeing * fix tests, and extract constant * return early * fixtest and checkstyle --------- Co-authored-by: Anna Maartensson <120831475+annamaartensson@users.noreply.github.com> Co-authored-by: Tian0602 <646432316@qq.com> Co-authored-by: LottaJohnsson <35195355+LottaJohnsson@users.noreply.github.com> Co-authored-by: Filippa Nilsson Co-authored-by: Filippa Nilsson <75281470+filippanilsson@users.noreply.github.com> Co-authored-by: Oliver Kopp Co-authored-by: Siedlerchr --- CHANGELOG.md | 1 + build.gradle | 3 + licenses/com.googlecode.plist_ddplist.txt | 20 + src/main/java/module-info.java | 1 + .../bibtex/comparator/BibDatabaseDiff.java | 6 +- .../importer/fileformat/BibtexParser.java | 195 ++++++++-- .../MetadataSerializationConfiguration.java | 5 + .../jabref/model/groups/AllEntriesGroup.java | 6 + .../jabref/model/groups/GroupTreeNode.java | 12 + .../org/jabref/model/metadata/MetaData.java | 1 + .../importer/fileformat/BibtexParserTest.java | 349 +++++++++++++++++- 11 files changed, 561 insertions(+), 38 deletions(-) create mode 100644 licenses/com.googlecode.plist_ddplist.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f1642cc7cfc..03424a9bd35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - When pasting HTML into the abstract or a comment field, the hypertext is automatically converted to Markdown. [#10558](https://github.com/JabRef/jabref/issues/10558) - We added the possibility to redownload files that had been present but are no longer in the specified location. [#10848](https://github.com/JabRef/jabref/issues/10848) - We added the citation key pattern `[camelN]`. Equivalent to the first N words of the `[camel]` pattern. +- We added importing of static groups and linked files from BibDesk .bib files. [#10381](https://github.com/JabRef/jabref/issues/10381) - We added ability to export in CFF (Citation File Format) [#10661](https://github.com/JabRef/jabref/issues/10661). - We added ability to push entries to TeXworks. [#3197](https://github.com/JabRef/jabref/issues/3197) - We added the ability to zoom in and out in the document viewer using Ctrl + Scroll. [#10964](https://github.com/JabRef/jabref/pull/10964) diff --git a/build.gradle b/build.gradle index a2e998897ae..61eeaafc33f 100644 --- a/build.gradle +++ b/build.gradle @@ -242,6 +242,9 @@ dependencies { // Because of GraalVM quirks, we need to ship that. See https://github.com/jspecify/jspecify/issues/389#issuecomment-1661130973 for details implementation 'org.jspecify:jspecify:0.3.0' + // parse plist files + implementation 'com.googlecode.plist:dd-plist:1.23' + 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/licenses/com.googlecode.plist_ddplist.txt b/licenses/com.googlecode.plist_ddplist.txt new file mode 100644 index 00000000000..ab9e4668533 --- /dev/null +++ b/licenses/com.googlecode.plist_ddplist.txt @@ -0,0 +1,20 @@ +dd-plist - An open source library to parse and generate property lists +Copyright (C) 2016 Daniel Dreibrodt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 17a0f833a57..fd4dbe1b0c2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -144,4 +144,5 @@ requires org.libreoffice.uno; requires de.saxsys.mvvmfx.validation; requires com.jthemedetector; + requires dd.plist; } diff --git a/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java b/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java index 7645ed85d1c..b807124cfc3 100644 --- a/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java +++ b/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java @@ -49,6 +49,11 @@ private static EntryComparator getEntryComparator() { private static List compareEntries(List originalEntries, List newEntries, BibDatabaseMode mode) { List differences = new ArrayList<>(); + // Prevent IndexOutOfBoundException + if (newEntries.isEmpty()) { + return differences; + } + // Create a HashSet where we can put references to entries in the new // database that we have matched. This is to avoid matching them twice. Set used = new HashSet<>(newEntries.size()); @@ -88,7 +93,6 @@ private static List compareEntries(List originalEntries, } } } - BibEntry bestEntry = newEntries.get(bestMatchIndex); if (bestMatch > MATCH_THRESHOLD || hasEqualCitationKey(originalEntry, bestEntry) diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index 0a32dd95989..fa535dab56b 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -1,10 +1,13 @@ package org.jabref.logic.importer.fileformat; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackReader; import java.io.Reader; import java.io.StringWriter; +import java.nio.file.Path; +import java.util.Base64; import java.util.Collection; import java.util.Deque; import java.util.HashMap; @@ -16,12 +19,17 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.regex.Pattern; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import org.jabref.logic.bibtex.FieldContentFormatter; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.exporter.BibtexDatabaseWriter; import org.jabref.logic.exporter.SaveConfiguration; +import org.jabref.logic.groups.DefaultGroupsFactory; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParseException; @@ -35,17 +43,31 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryType; import org.jabref.model.entry.BibtexString; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; import org.jabref.model.entry.field.FieldProperty; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.EntryTypeFactory; +import org.jabref.model.groups.ExplicitGroup; +import org.jabref.model.groups.GroupHierarchyType; +import org.jabref.model.groups.GroupTreeNode; import org.jabref.model.metadata.MetaData; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.model.util.FileUpdateMonitor; +import com.dd.plist.BinaryPropertyListParser; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import static org.jabref.logic.util.MetadataSerializationConfiguration.GROUP_QUOTE_CHAR; +import static org.jabref.logic.util.MetadataSerializationConfiguration.GROUP_TYPE_SUFFIX; /** * Class for importing BibTeX-files. @@ -68,8 +90,8 @@ */ public class BibtexParser implements Parser { private static final Logger LOGGER = LoggerFactory.getLogger(BibtexParser.class); - private static final Integer LOOKAHEAD = 1024; + private static final String BIB_DESK_ROOT_GROUP_NAME = "BibDeskGroups"; private final FieldContentFormatter fieldContentFormatter; private final Deque pureTextFromFile = new LinkedList<>(); private final ImportFormatPreferences importFormatPreferences; @@ -80,11 +102,16 @@ public class BibtexParser implements Parser { private int line = 1; private ParserResult parserResult; private final MetaDataParser metaDataParser; + private final Map parsedBibdeskGroups; + + private GroupTreeNode bibDeskGroupTreeNode; + private final DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); public BibtexParser(ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor) { this.importFormatPreferences = Objects.requireNonNull(importFormatPreferences); this.fieldContentFormatter = new FieldContentFormatter(importFormatPreferences.fieldPreferences()); this.metaDataParser = new MetaDataParser(fileMonitor); + this.parsedBibdeskGroups = new HashMap<>(); } public BibtexParser(ImportFormatPreferences importFormatPreferences) { @@ -209,28 +236,51 @@ private ParserResult parseFileContent() throws IOException { // Try to read the entry type String entryType = parseTextToken().toLowerCase(Locale.ROOT).trim(); - if ("preamble".equals(entryType)) { - database.setPreamble(parsePreamble()); - // Consume a new line which separates the preamble from the next part (if the file was written with JabRef) - skipOneNewline(); - // the preamble is saved verbatim anyway, so the text read so far can be dropped - dumpTextReadSoFarToString(); - } else if ("string".equals(entryType)) { - parseBibtexString(); - } else if ("comment".equals(entryType)) { - parseJabRefComment(meta); - } else { - // Not a comment, preamble, or string. Thus, it is an entry - parseAndAddEntry(entryType); + switch (entryType) { + case "preamble" -> { + database.setPreamble(parsePreamble()); + // Consume a new line which separates the preamble from the next part (if the file was written with JabRef) + skipOneNewline(); + // the preamble is saved verbatim anyway, so the text read so far can be dropped + dumpTextReadSoFarToString(); + } + case "string" -> + parseBibtexString(); + case "comment" -> + parseJabRefComment(meta); + default -> + // Not a comment, preamble, or string. Thus, it is an entry + parseAndAddEntry(entryType); } skipWhitespace(); } + addBibDeskGroupEntriesToJabRefGroups(); + try { - parserResult.setMetaData(metaDataParser.parse( + MetaData metaData = metaDataParser.parse( meta, - importFormatPreferences.bibEntryPreferences().getKeywordSeparator())); + importFormatPreferences.bibEntryPreferences().getKeywordSeparator()); + if (bibDeskGroupTreeNode != null) { + metaData.getGroups().ifPresentOrElse(existingGroupTree -> { + var existingGroups = meta.get(MetaData.GROUPSTREE); + // We only have one Group BibDeskGroup with n children + // instead of iterating through the whole group structure every time we just search in the metadata for the group name + var groupsToAdd = bibDeskGroupTreeNode.getChildren() + .stream(). + filter(Predicate.not(groupTreeNode -> existingGroups.contains(GROUP_TYPE_SUFFIX + groupTreeNode.getName() + GROUP_QUOTE_CHAR))); + groupsToAdd.forEach(existingGroupTree::addChild); + }, + // metadata does not contain any groups, so we need to create an AllEntriesGroup and add the other groups as children + () -> { + GroupTreeNode rootNode = new GroupTreeNode(DefaultGroupsFactory.getAllEntriesGroup()); + bibDeskGroupTreeNode.moveTo(rootNode); + metaData.setGroups(rootNode); + } + ); + } + parserResult.setMetaData(metaData); } catch (ParseException exception) { parserResult.addException(exception); } @@ -282,7 +332,6 @@ private void parseAndAddEntry(String type) { } catch (IOException ex) { // This makes the parser more robust: // If an exception is thrown when parsing an entry, drop the entry and try to resume parsing. - LOGGER.warn("Could not parse entry", ex); parserResult.addWarning(Localization.lang("Error occurred when parsing entry") + ": '" + ex.getMessage() + "'. " + "\n\n" + Localization.lang("JabRef skipped the entry.")); @@ -330,6 +379,73 @@ private void parseJabRefComment(Map meta) { // custom entry types are always re-written by JabRef and not stored in the file dumpTextReadSoFarToString(); + } else if (comment.startsWith(MetaData.BIBDESK_STATIC_FLAG)) { + try { + parseBibDeskComment(comment, meta); + } catch (ParseException ex) { + parserResult.addException(ex); + } + } + } + + /** + * Adds BibDesk group entries to the JabRef database + */ + private void addBibDeskGroupEntriesToJabRefGroups() { + for (String groupName : parsedBibdeskGroups.keySet()) { + String[] citationKeys = parsedBibdeskGroups.get(groupName).split(","); + for (String citation : citationKeys) { + Optional bibEntry = database.getEntryByCitationKey(citation); + Optional groupValue = bibEntry.flatMap(entry -> entry.getField(StandardField.GROUPS)); + if (groupValue.isEmpty()) { // if the citation does not belong to a group already + bibEntry.flatMap(entry -> entry.setField(StandardField.GROUPS, groupName)); + } else if (!groupValue.get().contains(groupName)) { + // if the citation does belong to a group already and is not yet assigned to the same group, we concatenate + String concatGroup = groupValue.get() + "," + groupName; + bibEntry.flatMap(entryByCitationKey -> entryByCitationKey.setField(StandardField.GROUPS, concatGroup)); + } + } + } + } + + /** + * Parses comment types found in BibDesk, to migrate BibDesk Static Groups to JabRef. + */ + private void parseBibDeskComment(String comment, Map meta) throws ParseException { + String xml = comment.substring(MetaData.BIBDESK_STATIC_FLAG.length() + 1, comment.length() - 1); + try { + // Build a document to handle the xml tags + Document doc = builder.newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes())); + doc.getDocumentElement().normalize(); + + NodeList dictList = doc.getElementsByTagName("dict"); + meta.putIfAbsent(MetaData.DATABASE_TYPE, "bibtex;"); + bibDeskGroupTreeNode = GroupTreeNode.fromGroup(new ExplicitGroup(BIB_DESK_ROOT_GROUP_NAME, GroupHierarchyType.INDEPENDENT, importFormatPreferences.bibEntryPreferences().getKeywordSeparator())); + + // Since each static group has their own dict element, we iterate through them + for (int i = 0; i < dictList.getLength(); i++) { + Element dictElement = (Element) dictList.item(i); + NodeList keyList = dictElement.getElementsByTagName("key"); + NodeList stringList = dictElement.getElementsByTagName("string"); + + String groupName = null; + String citationKeys = null; + + // Retrieves group name and group entries and adds these to the metadata + for (int j = 0; j < keyList.getLength(); j++) { + if (keyList.item(j).getTextContent().matches("group name")) { + groupName = stringList.item(j).getTextContent(); + var staticGroup = new ExplicitGroup(groupName, GroupHierarchyType.INDEPENDENT, importFormatPreferences.bibEntryPreferences().getKeywordSeparator()); + bibDeskGroupTreeNode.addSubgroup(staticGroup); + } else if (keyList.item(j).getTextContent().matches("keys")) { + citationKeys = stringList.item(j).getTextContent(); // adds group entries + } + } + // Adds the group name and citation keys to the field so all the entries can be added in the groups once parsed + parsedBibdeskGroups.putIfAbsent(groupName, citationKeys); + } + } catch (ParserConfigurationException | IOException | SAXException e) { + throw new ParseException(e); } } @@ -618,13 +734,29 @@ private void parseField(BibEntry entry) throws IOException { // it inconvenient // for users if JabRef did not accept it. if (field.getProperties().contains(FieldProperty.PERSON_NAMES)) { - entry.setField(field, entry.getField(field).get() + " and " + content); + entry.setField(field, entry.getField(field).orElse("") + " and " + content); } else if (StandardField.KEYWORDS == field) { // multiple keywords fields should be combined to one entry.addKeyword(content, importFormatPreferences.bibEntryPreferences().getKeywordSeparator()); } } else { - entry.setField(field, content); + // If a BibDesk File Field is encountered + if (field.getName().length() > 10 && field.getName().startsWith("bdsk-file-")) { + byte[] decodedBytes = Base64.getDecoder().decode(content); + try { + // Parse the base64 encoded binary plist to get the relative (to the .bib file) path + NSDictionary plist = (NSDictionary) BinaryPropertyListParser.parse(decodedBytes); + NSString relativePath = (NSString) plist.objectForKey("relativePath"); + Path path = Path.of(relativePath.getContent()); + + LinkedFile file = new LinkedFile("", path, ""); + entry.addFile(file); + } catch (Exception e) { + throw new IOException(); + } + } else { + entry.setField(field, content); + } } } } @@ -774,7 +906,6 @@ private String fixKey() throws IOException { /** * returns a new StringBuilder which corresponds to toRemove without whitespaces - * */ private StringBuilder removeWhitespaces(StringBuilder toRemove) { StringBuilder result = new StringBuilder(); @@ -919,20 +1050,16 @@ private StringBuilder parseBracketedFieldContent() throws IOException { // Check for "\},\n" - Example context: ` path = {c:\temp\},\n` // On Windows, it could be "\},\r\n", thus we rely in OS.NEWLINE.charAt(0) (which returns '\r' or '\n'). // In all cases, we should check for '\n' as the file could be encoded with Linux line endings on Windows. - if ((nextTwoCharacters[0] == ',') && ((nextTwoCharacters[1] == OS.NEWLINE.charAt(0)) || (nextTwoCharacters[1] == '\n'))) { - // We hit '\}\r` or `\}\n` - // Heuristics: Unwanted escaping of } - // - // Two consequences: - // - // 1. Keep `\` as read - // This is already done - // - // 2. Treat `}` as closing bracket - isClosingBracket = true; - } else { - isClosingBracket = false; - } + // We hit '\}\r` or `\}\n` + // Heuristics: Unwanted escaping of } + // + // Two consequences: + // + // 1. Keep `\` as read + // This is already done + // + // 2. Treat `}` as closing bracket + isClosingBracket = (nextTwoCharacters[0] == ',') && ((nextTwoCharacters[1] == OS.NEWLINE.charAt(0)) || (nextTwoCharacters[1] == '\n')); } else { isClosingBracket = true; } diff --git a/src/main/java/org/jabref/logic/util/MetadataSerializationConfiguration.java b/src/main/java/org/jabref/logic/util/MetadataSerializationConfiguration.java index 8ca88821f12..db99eb45882 100644 --- a/src/main/java/org/jabref/logic/util/MetadataSerializationConfiguration.java +++ b/src/main/java/org/jabref/logic/util/MetadataSerializationConfiguration.java @@ -18,6 +18,11 @@ public class MetadataSerializationConfiguration { */ public static final char GROUP_QUOTE_CHAR = '\\'; + /** + * Group Type suffix (part of the GroupType) + */ + public static final String GROUP_TYPE_SUFFIX = ":"; + /** * For separating units (e.g. name and hierarchic context) in the string representation */ diff --git a/src/main/java/org/jabref/model/groups/AllEntriesGroup.java b/src/main/java/org/jabref/model/groups/AllEntriesGroup.java index 320b3306ce8..1e3ef85fa53 100644 --- a/src/main/java/org/jabref/model/groups/AllEntriesGroup.java +++ b/src/main/java/org/jabref/model/groups/AllEntriesGroup.java @@ -23,6 +23,12 @@ public boolean equals(Object o) { return o instanceof AllEntriesGroup aeg && Objects.equals(aeg.getName(), getName()); } + /** + * Always returns true for any BibEntry! + * + * @param entry The @{@link BibEntry} to check + * @return Always returns true + */ @Override public boolean contains(BibEntry entry) { return true; diff --git a/src/main/java/org/jabref/model/groups/GroupTreeNode.java b/src/main/java/org/jabref/model/groups/GroupTreeNode.java index efe4d233fee..d5954181d9d 100644 --- a/src/main/java/org/jabref/model/groups/GroupTreeNode.java +++ b/src/main/java/org/jabref/model/groups/GroupTreeNode.java @@ -135,6 +135,13 @@ public int hashCode() { return Objects.hash(group); } + /** + * Get only groups containing all the entries or just groups containing any of the + * + * @param entries List of {@link BibEntry} to search for + * @param requireAll Whether to return only groups that must contain all entries + * @return List of {@link GroupTreeNode} containing the matches. {@link AllEntriesGroup} is always contained} + */ public List getContainingGroups(List entries, boolean requireAll) { List groups = new ArrayList<>(); @@ -197,6 +204,11 @@ public List getEntriesInGroup(List entries) { return result; } + /** + * Get the name of the underlying group + * + * @return String the name of the group + */ public String getName() { return group.getName(); } diff --git a/src/main/java/org/jabref/model/metadata/MetaData.java b/src/main/java/org/jabref/model/metadata/MetaData.java index 0479400337a..5c7293487ad 100644 --- a/src/main/java/org/jabref/model/metadata/MetaData.java +++ b/src/main/java/org/jabref/model/metadata/MetaData.java @@ -48,6 +48,7 @@ public class MetaData { public static final String FILE_DIRECTORY_LATEX = "fileDirectoryLatex"; public static final String PROTECTED_FLAG_META = "protectedFlag"; public static final String SELECTOR_META_PREFIX = "selector_"; + public static final String BIBDESK_STATIC_FLAG = "BibDesk Static Groups"; public static final char ESCAPE_CHARACTER = '\\'; public static final char SEPARATOR_CHARACTER = ';'; diff --git a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java index 9ec42a04d89..3c84de494c2 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java @@ -31,6 +31,8 @@ import org.jabref.logic.importer.ParseException; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.util.OS; +import org.jabref.model.TreeNode; +import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryType; @@ -56,6 +58,7 @@ import org.jabref.model.metadata.SaveOrder; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; @@ -72,10 +75,10 @@ /** * Tests for reading whole bib files can be found at {@link org.jabref.logic.importer.fileformat.BibtexImporterTest} *

- * Tests cannot be executed concurrently, because Localization is used at {@link BibtexParser#parseAndAddEntry(String)} + * Tests cannot be executed concurrently, because Localization is used at {@link BibtexParser#sparseAndAddEntry(String)} */ class BibtexParserTest { - + private static final String BIB_DESK_ROOT_GROUP_NAME = "BibDeskGroups"; private ImportFormatPreferences importFormatPreferences; private BibtexParser parser; @@ -87,7 +90,7 @@ void setUp() { } @Test - void parseWithNullThrowsNullPointerException() throws Exception { + void parseWithNullThrowsNullPointerException() { Executable toBeTested = () -> parser.parse(null); assertThrows(NullPointerException.class, toBeTested); } @@ -1380,6 +1383,311 @@ void integrationTestGroupTree() throws IOException, ParseException { ((ExplicitGroup) root.getChildren().get(2).getGroup()).getLegacyEntryKeys()); } + /** + * Checks that BibDesk Static Groups are available after parsing the library + */ + @Test + void integrationTestBibDeskStaticGroup() throws Exception { + ParserResult result = parser.parse(new StringReader(""" + @article{Swain:2023aa, + author = {Subhashree Swain and P. Shalima and K.V.P. Latha}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06758}, + month = {09}, + title = {Unravelling the Nuclear Dust Morphology of NGC 1365: A Two Phase Polar - RAT Model for the Ultraviolet to Infrared Spectral Energy Distribution}, + url = {https://arxiv.org/pdf/2309.06758.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06758.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06758}} + + @article{Heyl:2023aa, + author = {Johannes Heyl and Joshua Butterworth and Serena Viti}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06784}, + month = {09}, + title = {Understanding Molecular Abundances in Star-Forming Regions Using Interpretable Machine Learning}, + url = {https://arxiv.org/pdf/2309.06784.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06784.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06784}} + + @comment{BibDesk Static Groups{ + + + + + + group name + firstTestGroup + keys + Swain:2023aa,Heyl:2023aa + + + group name + secondTestGroup + keys + Swain:2023aa + + + + }} + """)); + + GroupTreeNode root = result.getMetaData().getGroups().get(); + assertEquals(new AllEntriesGroup("All entries"), root.getGroup()); + assertEquals(Optional.of(BIB_DESK_ROOT_GROUP_NAME), root.getFirstChild().map(GroupTreeNode::getName)); + + ExplicitGroup firstTestGroupExpected = new ExplicitGroup("firstTestGroup", GroupHierarchyType.INDEPENDENT, ','); + firstTestGroupExpected.setExpanded(true); + + assertEquals(Optional.of(firstTestGroupExpected), root.getFirstChild().flatMap(TreeNode::getFirstChild).map(GroupTreeNode::getGroup)); + + ExplicitGroup secondTestGroupExpected = new ExplicitGroup("secondTestGroup", GroupHierarchyType.INDEPENDENT, ','); + secondTestGroupExpected.setExpanded(true); + assertEquals(Optional.of(secondTestGroupExpected), root.getFirstChild().flatMap(TreeNode::getLastChild).map(GroupTreeNode::getGroup)); + + BibDatabase db = result.getDatabase(); + + assertEquals(List.of(root.getGroup(), firstTestGroupExpected), root.getContainingGroups(db.getEntries(), true).stream().map(GroupTreeNode::getGroup).toList()); + assertEquals(List.of(root.getGroup(), firstTestGroupExpected), root.getContainingGroups(db.getEntryByCitationKey("Heyl:2023aa").stream().toList(), false).stream().map(GroupTreeNode::getGroup).toList()); + } + + /** + * Checks that BibDesk Smart Groups are available after parsing the library + */ + @Test + @Disabled("Not yet supported") + void integrationTestBibDeskSmartGroup() throws Exception { + ParserResult result = parser.parse(new StringReader(""" + @article{Kraljic:2023aa, + author = {Katarina Kraljic and Florent Renaud and Yohan Dubois and Christophe Pichon and Oscar Agertz and Eric Andersson and Julien Devriendt and Jonathan Freundlich and Sugata Kaviraj and Taysun Kimm and Garreth Martin and S{\\'e}bastien Peirani and {\\'A}lvaro Segovia Otero and Marta Volonteri and Sukyoung K. Yi}, + date-added = {2023-09-14 20:09:10 +0200}, + date-modified = {2023-09-14 20:09:10 +0200}, + eprint = {2309.06485}, + month = {09}, + title = {Emergence and cosmic evolution of the Kennicutt-Schmidt relation driven by interstellar turbulence}, + url = {https://arxiv.org/pdf/2309.06485.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06485.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06485}} + + @article{Swain:2023aa, + author = {Subhashree Swain and P. Shalima and K.V.P. Latha}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06758}, + month = {09}, + title = {Unravelling the Nuclear Dust Morphology of NGC 1365: A Two Phase Polar - RAT Model for the Ultraviolet to Infrared Spectral Energy Distribution}, + url = {https://arxiv.org/pdf/2309.06758.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06758.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06758}} + + @article{Heyl:2023aa, + author = {Johannes Heyl and Joshua Butterworth and Serena Viti}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06784}, + month = {09}, + title = {Understanding Molecular Abundances in Star-Forming Regions Using Interpretable Machine Learning}, + url = {https://arxiv.org/pdf/2309.06784.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06784.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06784}} + + @comment{BibDesk Smart Groups{ + + + + + + conditions + + + comparison + 4 + key + BibTeX Type + value + article + version + 1 + + + comparison + 2 + key + Title + value + the + version + 1 + + + conjunction + 0 + group name + article + + + conditions + + + comparison + 3 + key + Author + value + Swain + version + 1 + + + conjunction + 0 + group name + Swain + + + + }} + """)); + + GroupTreeNode root = result.getMetaData().getGroups().get(); + assertEquals(new AllEntriesGroup("All entries"), root.getGroup()); + assertEquals(2, root.getNumberOfChildren()); + ExplicitGroup firstTestGroupExpected = new ExplicitGroup("article", GroupHierarchyType.INDEPENDENT, ','); + firstTestGroupExpected.setExpanded(false); + assertEquals(firstTestGroupExpected, root.getChildren().get(0).getGroup()); + ExplicitGroup secondTestGroupExpected = new ExplicitGroup("Swain", GroupHierarchyType.INDEPENDENT, ','); + secondTestGroupExpected.setExpanded(false); + assertEquals(secondTestGroupExpected, root.getChildren().get(1).getGroup()); + + BibDatabase db = result.getDatabase(); + List firstTestGroupEntriesExpected = new ArrayList<>(); + firstTestGroupEntriesExpected.add(db.getEntryByCitationKey("Kraljic:2023aa").get()); + firstTestGroupEntriesExpected.add(db.getEntryByCitationKey("Swain:2023aa").get()); + assertTrue(root.getChildren().get(0).getGroup().containsAll(firstTestGroupEntriesExpected)); + assertFalse(root.getChildren().get(1).getGroup().contains(db.getEntryByCitationKey("Swain:2023aa").get())); + } + + /** + * Checks that both BibDesk Static Groups and Smart Groups are available after parsing the library + */ + @Test + @Disabled("Not yet supported") + void integrationTestBibDeskMultipleGroup() throws Exception { + ParserResult result = parser.parse(new StringReader(""" + @article{Kraljic:2023aa, + author = {Katarina Kraljic and Florent Renaud and Yohan Dubois and Christophe Pichon and Oscar Agertz and Eric Andersson and Julien Devriendt and Jonathan Freundlich and Sugata Kaviraj and Taysun Kimm and Garreth Martin and S{\\'e}bastien Peirani and {\\'A}lvaro Segovia Otero and Marta Volonteri and Sukyoung K. Yi}, + date-added = {2023-09-14 20:09:10 +0200}, + date-modified = {2023-09-14 20:09:10 +0200}, + eprint = {2309.06485}, + month = {09}, + title = {Emergence and cosmic evolution of the Kennicutt-Schmidt relation driven by interstellar turbulence}, + url = {https://arxiv.org/pdf/2309.06485.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06485.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06485}} + + @article{Swain:2023aa, + author = {Subhashree Swain and P. Shalima and K.V.P. Latha}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06758}, + month = {09}, + title = {Unravelling the Nuclear Dust Morphology of NGC 1365: A Two Phase Polar - RAT Model for the Ultraviolet to Infrared Spectral Energy Distribution}, + url = {https://arxiv.org/pdf/2309.06758.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06758.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06758}} + + @article{Heyl:2023aa, + author = {Johannes Heyl and Joshua Butterworth and Serena Viti}, + date-added = {2023-09-14 20:09:08 +0200}, + date-modified = {2023-09-14 20:09:08 +0200}, + eprint = {2309.06784}, + month = {09}, + title = {Understanding Molecular Abundances in Star-Forming Regions Using Interpretable Machine Learning}, + url = {https://arxiv.org/pdf/2309.06784.pdf}, + year = {2023}, + bdsk-url-1 = {https://arxiv.org/pdf/2309.06784.pdf}, + bdsk-url-2 = {https://arxiv.org/abs/2309.06784}} + + @comment{BibDesk Static Groups{ + + + + + + group name + firstTestGroup + keys + Swain:2023aa,Heyl:2023aa + + + + }} + + @comment{BibDesk Smart Groups{ + + + + + + conditions + + + comparison + 4 + key + BibTeX Type + value + article + version + 1 + + + comparison + 2 + key + Title + value + the + version + 1 + + + conjunction + 0 + group name + article + + + + }} + """)); + + GroupTreeNode root = result.getMetaData().getGroups().get(); + assertEquals(new AllEntriesGroup("All entries"), root.getGroup()); + assertEquals(2, root.getNumberOfChildren()); + ExplicitGroup firstTestGroupExpected = new ExplicitGroup("firstTestGroup", GroupHierarchyType.INDEPENDENT, ','); + firstTestGroupExpected.setExpanded(false); + assertEquals(firstTestGroupExpected, root.getChildren().get(0).getGroup()); + ExplicitGroup secondTestGroupExpected = new ExplicitGroup("article", GroupHierarchyType.INDEPENDENT, ','); + secondTestGroupExpected.setExpanded(false); + assertEquals(secondTestGroupExpected, root.getChildren().get(1).getGroup()); + + BibDatabase db = result.getDatabase(); + assertTrue(root.getChildren().get(0).getGroup().containsAll(db.getEntries())); + List smartGroupEntriesExpected = new ArrayList<>(); + smartGroupEntriesExpected.add(db.getEntryByCitationKey("Kraljic:2023aa").get()); + smartGroupEntriesExpected.add(db.getEntryByCitationKey("Swain:2023aa").get()); + assertTrue(root.getChildren().get(0).getGroup().containsAll(smartGroupEntriesExpected)); + } + /** * Checks that a TexGroup finally gets the required data, after parsing the library. */ @@ -1863,4 +2171,39 @@ void parseDuplicateKeywordsWithTwoEntries() throws Exception { ParserResult result = parser.parse(new StringReader(entries)); assertEquals(List.of(expectedEntryFirst, expectedEntrySecond), result.getDatabase().getEntries()); } + + @Test + void parseBibDeskLinkedFiles() throws IOException { + + BibEntry expectedEntry = new BibEntry(StandardEntryType.Article); + expectedEntry.withCitationKey("Kovakkuni:2023aa") + .withField(StandardField.AUTHOR, "Navyasree Kovakkuni and Federico Lelli and Pierre-alain Duc and M{\\'e}d{\\'e}ric Boquien and Jonathan Braine and Elias Brinks and Vassilis Charmandaris and Francoise Combes and Jeremy Fensch and Ute Lisenfeld and Stacy McGaugh and J. Chris Mihos and Marcel. S. Pawlowski and Yves. Revaz and Peter. M. Weilbacher") + .withField(new UnknownField("date-added"), "2023-09-14 20:09:12 +0200") + .withField(new UnknownField("date-modified"), "2023-09-14 20:09:12 +0200") + .withField(StandardField.EPRINT, "2309.06478") + .withField(StandardField.MONTH, "09") + .withField(StandardField.TITLE, "Molecular and Ionized Gas in Tidal Dwarf Galaxies: The Spatially Resolved Star-Formation Relation") + .withField(StandardField.URL, "https://arxiv.org/pdf/2309.06478.pdf") + .withField(StandardField.YEAR, "2023") + .withField(new UnknownField("bdsk-url-1"), "https://arxiv.org/abs/2309.06478") + .withField(StandardField.FILE, ":../../Downloads/2309.06478.pdf:"); + + ParserResult result = parser.parse(new StringReader(""" + @article{Kovakkuni:2023aa, + author = {Navyasree Kovakkuni and Federico Lelli and Pierre-alain Duc and M{\\'e}d{\\'e}ric Boquien and Jonathan Braine and Elias Brinks and Vassilis Charmandaris and Francoise Combes and Jeremy Fensch and Ute Lisenfeld and Stacy McGaugh and J. Chris Mihos and Marcel. S. Pawlowski and Yves. Revaz and Peter. M. Weilbacher}, + date-added = {2023-09-14 20:09:12 +0200}, + date-modified = {2023-09-14 20:09:12 +0200}, + eprint = {2309.06478}, + month = {09}, + title = {Molecular and Ionized Gas in Tidal Dwarf Galaxies: The Spatially Resolved Star-Formation Relation}, + url = {https://arxiv.org/pdf/2309.06478.pdf}, + year = {2023}, + bdsk-file-1 = {YnBsaXN0MDDSAQIDBFxyZWxhdGl2ZVBhdGhZYWxpYXNEYXRhXxAeLi4vLi4vRG93bmxvYWRzLzIzMDkuMDY0NzgucGRmTxEBUgAAAAABUgACAAAMTWFjaW50b3NoIEhEAAAAAAAAAAAAAAAAAAAA4O/yLkJEAAH/////DjIzMDkuMDY0NzgucGRmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////hKRkeAAAAAAAAAAAAAgACAAAKIGN1AAAAAAAAAAAAAAAAAAlEb3dubG9hZHMAAAIAKy86VXNlcnM6Y2hyaXN0b3BoczpEb3dubG9hZHM6MjMwOS4wNjQ3OC5wZGYAAA4AHgAOADIAMwAwADkALgAwADYANAA3ADgALgBwAGQAZgAPABoADABNAGEAYwBpAG4AdABvAHMAaAAgAEgARAASAClVc2Vycy9jaHJpc3RvcGhzL0Rvd25sb2Fkcy8yMzA5LjA2NDc4LnBkZgAAEwABLwAAFQACABH//wAAAAgADQAaACQARQAAAAAAAAIBAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAGb}, + bdsk-url-1 = {https://arxiv.org/abs/2309.06478}} + } + """)); + BibDatabase database = result.getDatabase(); + + assertEquals(Collections.singletonList(expectedEntry), database.getEntries()); + } } From 57f8a63c5c8190ba65973fb569db368132f5d83d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 17 Mar 2024 18:58:18 +0100 Subject: [PATCH 09/38] Speed up failure reporting (#11030) --- .github/workflows/tests.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3070c02b51c..1016762e4aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -171,11 +171,15 @@ jobs: run: xvfb-run --auto-servernum ./gradlew check -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x modernizer env: CI: "true" + - name: Prepare format failed test results + if: failure() + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: xml-twig-tools xsltproc + version: 1.0 - name: Format failed test results if: failure() - run: | - sudo apt-get install -qq -y xml-twig-tools xsltproc - scripts/after-failure.sh + run: scripts/after-failure.sh databasetests: name: Database tests runs-on: ubuntu-latest From 7a4be6d2d56c6f017732d3330e51f20d7980dbf9 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 18 Mar 2024 08:24:51 +0100 Subject: [PATCH 10/38] Fixes Zotero file handling for absolute paths (#11038) * Fixes Zotero file handling for absolute paths Fixes #10959 * checkstyle mimiimm * fix changelog * cannot fix --- CHANGELOG.md | 1 + .../jabref/logic/importer/util/FileFieldParser.java | 5 +++++ .../logic/importer/fileformat/BibtexParserTest.java | 2 +- .../logic/importer/util/FileFieldParserTest.java | 10 ++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03424a9bd35..7d858bfaaea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the `File -> Close library` menu item was not disabled when no library was open. [#10948](https://github.com/JabRef/jabref/issues/10948) - We fixed an issue where the Document Viewer would show the PDF in only half the window when maximized. [#10934](https://github.com/JabRef/jabref/issues/10934) - Clicking on the crossref and related tags in the entry editor jumps to the linked entry. [#5484](https://github.com/JabRef/jabref/issues/5484) [#9369](https://github.com/JabRef/jabref/issues/9369) +- We fixed an issue where JabRef could not parse absolute file paths from Zotero exports [#10959](https://github.com/JabRef/jabref/issues/10959) ### Removed diff --git a/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java b/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java index a180e51c9ad..242671f7ea2 100644 --- a/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java +++ b/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java @@ -102,6 +102,11 @@ public List parse() { // We are at the second : (position 3 in the example) and "just" add it to the current element charactersOfCurrentElement.append(c); windowsPath = true; + // special case for zotero absolute path on windows that do not have a colon in front + // e.g. A:\zotero\paper.pdf + } else if (charactersOfCurrentElement.length() == 1 && value.charAt(i + 1) == '\\') { + charactersOfCurrentElement.append(c); + windowsPath = true; } else { // We are in the next LinkedFile data element linkedFileData.add(charactersOfCurrentElement.toString()); diff --git a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java index 3c84de494c2..7a118622cfe 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java @@ -75,7 +75,7 @@ /** * Tests for reading whole bib files can be found at {@link org.jabref.logic.importer.fileformat.BibtexImporterTest} *

- * Tests cannot be executed concurrently, because Localization is used at {@link BibtexParser#sparseAndAddEntry(String)} + * Tests cannot be executed concurrently, because Localization is used at {@link BibtexParser#parseAndAddEntry(String)} */ class BibtexParserTest { private static final String BIB_DESK_ROOT_GROUP_NAME = "BibDeskGroups"; diff --git a/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java b/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java index 1f5e1b17a43..a530aa368c0 100644 --- a/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java +++ b/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java @@ -203,6 +203,16 @@ private static Stream stringsToParseTest() throws Exception { Arguments.of( Collections.singletonList(new LinkedFile("", "matheus.ea explicit.pdf", "", "https://arxiv.org/pdf/1109.0517.pdf")), ":matheus.ea explicit.pdf::https\\://arxiv.org/pdf/1109.0517.pdf" + ), + // Absolute path + Arguments.of( + Collections.singletonList(new LinkedFile("", "A:\\Zotero\\storage\\test.pdf", "")), + ":A:\\Zotero\\storage\\test.pdf" + ), + // zotero absolute path + Arguments.of( + Collections.singletonList(new LinkedFile("", "A:\\Zotero\\storage\\test.pdf", "")), + "A:\\Zotero\\storage\\test.pdf" ) ); } From 7abf13dea3cbeb45dce349c930aa9b6acf926a67 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 18 Mar 2024 12:00:42 +0100 Subject: [PATCH 11/38] Change copy-paste function to handle string constants (follow up PR) (#11037) * [Copy] Include string constants in copy (#11) Signed-off-by: Anders Blomqvist * [Copy] New method for serializing string constants (#12) Signed-off-by: Anders Blomqvist * Add a sanity check for null for clipboard content Currenlty, the clipboard content can be null since the database does not seem to be updating. This is a sanity check to prevent the program from adding null to the clipboard. Link to DD2480-Group1/jabref#13 * [Fix] Add parsed serilization when save settings When loading from existing files or libraries, the parser will set the serilization of the string constant to the correct value. However, when editing via the GUI, the serilization was not set and a new string constant list will be created without the serilization. This result in the serilization being null and when copying with the clipboard. Link to DD2480-Group1/jabref#13 * feat: import string constants when pasting #9 Add functionality to import string constants in the paste function Should add functionality to handle colliding string constants. Should also check that the constants are valid using the ConstantsItemModel class. * feat: Add string constant validity checker and dialog messages #9 Check that a pasted string constant is valid using the ConstantsItemModel class. Add diagnostic messages notifying users when adding a string constant fails while pasting. * [Copy] Copy referenced constant strings to clipboard (#16) * feat: Add parsed serialized string when cloning * feat: Add sanity check for null in ClipBoardManager * closes #15 * feat: new unit tests Add 4 new unit tests, testing the new features added for issue-10872. Specifically the tests are for the `storeSettings` method in the ConstantsPropertiesViewModel.java, and `setContent` in the ClipBaordManager.java. Closes #6 * Update CHANGELOG with copy and paste function * Fix Checkstyle failing by reformat the code * Fix OpenRewrite failing by running rewriteRun * Refactor by extract methods in setContent * collet failures * changelog and use os.newline * checkstyle * use real bibentrytypes manager * Fix CHANGELOG.md * Swap if branches * Code cleanup * Use List for getUsedStringValues * Fix submodule * Collection is better * Fix csl-styles * Remove empty line * Group BibTeX string l10n together --------- Signed-off-by: Anders Blomqvist Co-authored-by: Anders Blomqvist Co-authored-by: ZOU Hetai <33616271+JXNCTED@users.noreply.github.com> Co-authored-by: Hannes Stig Co-authored-by: Elliot Co-authored-by: Oliver Kopp --- CHANGELOG.md | 1 + .../java/org/jabref/gui/ClipBoardManager.java | 22 ++- .../jabref/gui/edit/ReplaceStringAction.java | 1 - .../gui/externalfiles/ImportHandler.java | 30 +++- .../ConstantsPropertiesViewModel.java | 12 +- .../org/jabref/gui/maintable/MainTable.java | 12 +- .../importer/fileformat/BibtexParser.java | 4 + .../jabref/model/database/BibDatabase.java | 21 ++- .../org/jabref/model/entry/BibtexString.java | 3 + src/main/resources/l10n/JabRef_en.properties | 57 ++++---- .../org/jabref/gui/ClipBoardManagerTest.java | 128 ++++++++++++++++++ .../ConstantsPropertiesViewModelTest.java | 62 +++++++++ .../model/database/BibDatabaseTest.java | 12 +- 13 files changed, 299 insertions(+), 66 deletions(-) create mode 100644 src/test/java/org/jabref/gui/ClipBoardManagerTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d858bfaaea..2a4d245079d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added the ability to zoom in and out in the document viewer using Ctrl + Scroll. [#10964](https://github.com/JabRef/jabref/pull/10964) - We added a Cleanup for removing non-existent files and grouped the related options [#10929](https://github.com/JabRef/jabref/issues/10929) - We added the functionality to parse the bibliography of PDFs using the GROBID online service. [#10200](https://github.com/JabRef/jabref/issues/10200) +- We added support for BibTeX String constants during copy & paste between libraries [#10872](https://github.com/JabRef/jabref/issues/10872) ### Changed diff --git a/src/main/java/org/jabref/gui/ClipBoardManager.java b/src/main/java/org/jabref/gui/ClipBoardManager.java index 792c4733384..f90dc1b0f36 100644 --- a/src/main/java/org/jabref/gui/ClipBoardManager.java +++ b/src/main/java/org/jabref/gui/ClipBoardManager.java @@ -21,6 +21,7 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; @@ -155,14 +156,23 @@ public void setContent(String string) { } public void setContent(List entries, BibEntryTypesManager entryTypesManager) throws IOException { - final ClipboardContent content = new ClipboardContent(); - BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), entryTypesManager); - String serializedEntries = writer.serializeAll(entries, BibDatabaseMode.BIBTEX); + String serializedEntries = serializeEntries(entries, entryTypesManager); + setContent(serializedEntries); + } + + public void setContent(List entries, BibEntryTypesManager entryTypesManager, List stringConstants) throws IOException { + StringBuilder builder = new StringBuilder(); + stringConstants.forEach(strConst -> builder.append(strConst.getParsedSerialization() == null ? "" : strConst.getParsedSerialization())); + String serializedEntries = serializeEntries(entries, entryTypesManager); + builder.append(serializedEntries); + setContent(builder.toString()); + } + + private String serializeEntries(List entries, BibEntryTypesManager entryTypesManager) throws IOException { // BibEntry is not Java serializable. Thus, we need to do the serialization manually // At reading of the clipboard in JabRef, we parse the plain string in all cases, so we don't need to flag we put BibEntries here // Furthermore, storing a string also enables other applications to work with the data - content.putString(serializedEntries); - clipboard.setContent(content); - setPrimaryClipboardContent(content); + BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), entryTypesManager); + return writer.serializeAll(entries, BibDatabaseMode.BIBTEX); } } diff --git a/src/main/java/org/jabref/gui/edit/ReplaceStringAction.java b/src/main/java/org/jabref/gui/edit/ReplaceStringAction.java index 8339b5db7f3..3a7ba00795c 100644 --- a/src/main/java/org/jabref/gui/edit/ReplaceStringAction.java +++ b/src/main/java/org/jabref/gui/edit/ReplaceStringAction.java @@ -15,7 +15,6 @@ public class ReplaceStringAction extends SimpleCommand { public ReplaceStringAction(Supplier tabSupplier, StateManager stateManager, DialogService dialogService) { this.tabSupplier = tabSupplier; this.dialogService = dialogService; - this.executable.bind(ActionHelper.needsDatabase(stateManager)); } diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 18ac20841ee..b7fffe46b7f 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -18,6 +18,7 @@ import org.jabref.gui.StateManager; import org.jabref.gui.duplicationFinder.DuplicateResolverDialog; import org.jabref.gui.fieldeditors.LinkedFileViewModel; +import org.jabref.gui.libraryproperties.constants.ConstantsItemModel; import org.jabref.gui.undo.UndoableInsertEntries; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.DefaultTaskExecutor; @@ -40,7 +41,9 @@ import org.jabref.logic.util.io.FileUtil; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.KeyCollisionException; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibtexString; import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.ArXivIdentifier; @@ -311,13 +314,36 @@ private void generateKeys(List entries) { public List handleBibTeXData(String entries) { BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), fileUpdateMonitor); try { - return parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); + List result = parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); + Collection stringConstants = parser.getStringValues(); + importStringConstantsWithDuplicateCheck(stringConstants); + return result; } catch (ParseException ex) { LOGGER.error("Could not paste", ex); return Collections.emptyList(); } } + public void importStringConstantsWithDuplicateCheck(Collection stringConstants) { + List failures = new ArrayList<>(); + + for (BibtexString stringConstantToAdd : stringConstants) { + try { + ConstantsItemModel checker = new ConstantsItemModel(stringConstantToAdd.getName(), stringConstantToAdd.getContent()); + if (checker.combinedValidationValidProperty().get()) { + bibDatabaseContext.getDatabase().addString(stringConstantToAdd); + } else { + failures.add(Localization.lang("String constant \"%0\" was not imported because it is not a valid string constant", stringConstantToAdd.getName())); + } + } catch (KeyCollisionException ex) { + failures.add(Localization.lang("String constant %0 was not imported because it already exists in this library", stringConstantToAdd.getName())); + } + } + if (!failures.isEmpty()) { + dialogService.showWarningDialogAndWait(Localization.lang("Importing String constants"), Localization.lang("Could not import the following string constants:\n %0", String.join("\n", failures))); + } + } + public List handleStringData(String data) throws FetcherException { if ((data == null) || data.isEmpty()) { return Collections.emptyList(); @@ -356,7 +382,7 @@ private List tryImportFormats(String data) { } private List fetchByDOI(DOI doi) throws FetcherException { - LOGGER.info("Found DOI identifer in clipboard"); + LOGGER.info("Found DOI identifier in clipboard"); Optional entry = new DoiFetcher(preferencesService.getImportFormatPreferences()).performSearchById(doi.getDOI()); return OptionalUtil.toList(entry); } diff --git a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java index 36f4ddeff07..f29079199c8 100644 --- a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java +++ b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModel.java @@ -1,9 +1,9 @@ package org.jabref.gui.libraryproperties.constants; import java.util.Comparator; +import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ListProperty; @@ -18,6 +18,7 @@ import org.jabref.gui.libraryproperties.PropertiesTabViewModel; import org.jabref.logic.bibtex.comparator.BibtexStringComparator; import org.jabref.logic.help.HelpFile; +import org.jabref.logic.util.OS; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibtexString; import org.jabref.preferences.FilePreferences; @@ -86,9 +87,12 @@ private ConstantsItemModel convertFromBibTexString(BibtexString bibtexString) { @Override public void storeSettings() { - databaseContext.getDatabase().setStrings(stringsListProperty.stream() - .map(this::fromBibtexStringViewModel) - .collect(Collectors.toList())); + List strings = stringsListProperty.stream() + .map(this::fromBibtexStringViewModel) + .toList(); + strings.forEach(string -> string.setParsedSerialization("@String{" + + string.getName() + " = " + string.getContent() + "}" + OS.NEWLINE)); + databaseContext.getDatabase().setStrings(strings); } private BibtexString fromBibtexStringViewModel(ConstantsItemModel viewModel) { diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index e778fa13425..eb460dd0e94 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -51,6 +51,7 @@ import org.jabref.model.database.event.EntriesAddedEvent; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -257,8 +258,13 @@ public void copy() { List selectedEntries = getSelectedEntries(); if (!selectedEntries.isEmpty()) { + List stringConstants = getUsedStringValues(selectedEntries); try { - clipBoardManager.setContent(selectedEntries, entryTypesManager); + if (stringConstants.isEmpty()) { + clipBoardManager.setContent(selectedEntries, entryTypesManager); + } else { + clipBoardManager.setContent(selectedEntries, entryTypesManager, stringConstants); + } dialogService.notify(Localization.lang("Copied %0 entry(ies)", selectedEntries.size())); } catch (IOException e) { LOGGER.error("Error while copying selected entries to clipboard.", e); @@ -489,4 +495,8 @@ private Optional findEntry(BibEntry entry) { .filter(viewModel -> viewModel.getEntry().equals(entry)) .findFirst(); } + + private List getUsedStringValues(List entries) { + return database.getDatabase().getUsedStrings(entries); + } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index fa535dab56b..7669712a465 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -144,6 +144,10 @@ public List parseEntries(InputStream inputStream) throws ParseExceptio } } + public Collection getStringValues() { + return database.getStringValues(); + } + public Optional parseSingleEntry(String bibtexString) throws ParseException { return parseEntries(bibtexString).stream().findFirst(); } diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index 12f1ba3f08b..b28f6cc6398 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -370,27 +370,22 @@ public String resolveForStrings(String content) { /** * Get all strings used in the entries. */ - public Collection getUsedStrings(Collection entries) { - List result = new ArrayList<>(); + public List getUsedStrings(Collection entries) { Set allUsedIds = new HashSet<>(); - // All entries - for (BibEntry entry : entries) { - for (String fieldContent : entry.getFieldValues()) { - resolveContent(fieldContent, new HashSet<>(), allUsedIds); - } - } - // Preamble if (preamble != null) { resolveContent(preamble, new HashSet<>(), allUsedIds); } - for (String stringId : allUsedIds) { - result.add((BibtexString) bibtexStrings.get(stringId).clone()); + // All entries + for (BibEntry entry : entries) { + for (String fieldContent : entry.getFieldValues()) { + resolveContent(fieldContent, new HashSet<>(), allUsedIds); + } } - return result; + return allUsedIds.stream().map(bibtexStrings::get).toList(); } /** @@ -459,7 +454,7 @@ private String resolveString(String label, Set usedIds, Set allU // circular reference, and have to stop to avoid // infinite recursion. if (usedIds.contains(string.getId())) { - LOGGER.info("Stopped due to circular reference in strings: " + label); + LOGGER.info("Stopped due to circular reference in strings: {}", label); return label; } // If not, log this string's ID now. diff --git a/src/main/java/org/jabref/model/entry/BibtexString.java b/src/main/java/org/jabref/model/entry/BibtexString.java index d8e83fb6477..3515e0584e9 100644 --- a/src/main/java/org/jabref/model/entry/BibtexString.java +++ b/src/main/java/org/jabref/model/entry/BibtexString.java @@ -158,6 +158,9 @@ public String getUserComments() { public Object clone() { BibtexString clone = new BibtexString(name, content); clone.setId(id); + if (parsedSerialization != null) { + clone.setParsedSerialization(parsedSerialization); + } return clone; } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index e6c1342058c..d72fceb0f63 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -60,6 +60,30 @@ Added\ group\ "%0".=Added group "%0". Added\ string\:\ '%0'=Added string: '%0' Added\ string=Added string +Edit\ strings=Edit strings +Duplicate\ string\ name=Duplicate string name +Modified\ string=Modified string +Modified\ string\:\ '%0' =Modified string: '%0' +New\ string=New string +Remove\ string\ %0=Remove string %0 +Deleted\ string=Deleted string +Deleted\ string\:\ '%0'=Deleted string: '%0' +Renamed\ string\:\ '%0'=Renamed string: '%0' +Please\ enter\ the\ string's\ label=Please enter the string's label +Resolve\ BibTeX\ strings=Resolve BibTeX strings +The\ label\ of\ the\ string\ cannot\ be\ a\ number.=The label of the string cannot be a number. +The\ label\ of\ the\ string\ cannot\ contain\ spaces.=The label of the string cannot contain spaces. +The\ label\ of\ the\ string\ cannot\ contain\ the\ '\#'\ character.=The label of the string cannot contain the '#' character. +String\ dialog,\ add\ string=String dialog, add string +String\ dialog,\ remove\ string=String dialog, remove string +Add\ new\ String=Add new String +String\ constants=String constants +Must\ not\ be\ empty\!=Must not be empty\! +A\ string\ with\ the\ label\ '%0'\ already\ exists.=A string with the label '%0' already exists. +String\ constant\ "%0"\ was\ not\ imported\ because\ it\ is\ not\ a\ valid\ string\ constant=String constant "%0" was not imported because it is not a valid string constant +String\ constant\ %0\ was\ not\ imported\ because\ it\ already\ exists\ in\ this\ library=String constant %0 was not imported because it already exists in this library +Could\ not\ import\ the\ following\ string\ constants\:\n\ %0=Could not import the following string constants:\n %0 +Importing\ String\ constants=Importing String constants All\ entries=All entries @@ -260,8 +284,6 @@ Duplicate\ fields=Duplicate fields Unable\ to\ change\ field\ name\.\ "%0"\ already\ in\ use.=Unable to change field name. "%0" already in use. -Duplicate\ string\ name=Duplicate string name - Duplicates\ found=Duplicates found Dynamically\ group\ entries\ by\ a\ free-form\ search\ expression=Dynamically group entries by a free-form search expression @@ -276,8 +298,6 @@ Edit\ file\ type=Edit file type Edit\ group=Edit group -Edit\ strings=Edit strings - empty\ library=empty library Autocompletion=Autocompletion @@ -526,9 +546,6 @@ Modified\ group\ "%0".=Modified group "%0". Modified\ groups=Modified groups -Modified\ string=Modified string -Modified\ string\:\ '%0' =Modified string: '%0' - Modify=Modify move\ group=move group @@ -557,8 +574,6 @@ New\ BibTeX\ sublibrary=New BibTeX sublibrary New\ group=New group -New\ string=New string - Next\ entry=Next entry no\ base-BibTeX-file\ specified!=no base-BibTeX-file specified! @@ -631,8 +646,6 @@ File\ has\ no\ attached\ annotations=File has no attached annotations Please\ enter\ a\ name\ for\ the\ group.=Please enter a name for the group. -Please\ enter\ the\ string's\ label=Please enter the string's label - Please\ restart\ JabRef\ for\ preferences\ to\ take\ effect.=Please restart JabRef for preferences to take effect. Possible\ duplicate\ entries=Possible duplicate entries @@ -714,15 +727,8 @@ Remove\ group\ "%0"?=Remove group "%0"? Remove\ link=Remove link -Remove\ string\ %0=Remove string %0 - Removed\ group\ "%0".=Removed group "%0". -Deleted\ string=Deleted string -Deleted\ string\:\ '%0'=Deleted string: '%0' - -Renamed\ string\:\ '%0'=Renamed string: '%0' - Replace=Replace Replace\ With\:=Replace With: Limit\ to\ Selected\ Entries=Limit to Selected Entries @@ -741,8 +747,6 @@ Replaces\ Unicode\ ligatures\ with\ their\ expanded\ form=Replaces Unicode ligat Required\ fields=Required fields -Resolve\ BibTeX\ strings=Resolve BibTeX strings - resolved=resolved Restart=Restart @@ -893,12 +897,6 @@ Switches\ between\ full\ and\ abbreviated\ journal\ name\ if\ the\ journal\ name the\ field\ %0=the field %0 The\ group\ "%0"\ already\ contains\ the\ selection.=The group "%0" already contains the selection. -The\ label\ of\ the\ string\ cannot\ be\ a\ number.=The label of the string cannot be a number. - -The\ label\ of\ the\ string\ cannot\ contain\ spaces.=The label of the string cannot contain spaces. - -The\ label\ of\ the\ string\ cannot\ contain\ the\ '\#'\ character.=The label of the string cannot contain the '#' character. - The\ output\ option\ depends\ on\ a\ valid\ import\ option.=The output option depends on a valid import option. This\ operation\ requires\ all\ selected\ entries\ to\ have\ citation\ keys\ defined.=This operation requires all selected entries to have citation keys defined. @@ -1385,8 +1383,6 @@ Push\ to\ application=Push to application Refresh\ OpenOffice/LibreOffice=Refresh OpenOffice/LibreOffice Resolve\ duplicate\ citation\ keys=Resolve duplicate citation keys Save\ all=Save all -String\ dialog,\ add\ string=String dialog, add string -String\ dialog,\ remove\ string=String dialog, remove string Synchronize\ files=Synchronize files Unabbreviate=Unabbreviate should\ contain\ a\ protocol=should contain a protocol @@ -1978,8 +1974,6 @@ Total\ items\ found\:=Total items found: Selected\ items\:=Selected items: Download\ linked\ online\ files=Download linked online files Select\ the\ entries\ to\ be\ imported\:=Select the entries to be imported\: -Add\ new\ String=Add new String -Must\ not\ be\ empty\!=Must not be empty\! Open\ Help\ page=Open Help page Add\ new\ keyword=Add new keyword Keyword\:=Keyword: @@ -1988,7 +1982,6 @@ Keyword\ separator=Keyword separator Remove\ keyword=Remove keyword Are\ you\ sure\ you\ want\ to\ remove\ keyword\:\ \"%0\"?=Are you sure you want to remove keyword: "%0"? Reset\ to\ default=Reset to default -String\ constants=String constants Export\ all\ entries=Export all entries Generate\ citation\ keys=Generate citation keys New\ library=New library @@ -2008,8 +2001,6 @@ Set\ rank\ to\ three=Set rank to three Set\ rank\ to\ four=Set rank to four Set\ rank\ to\ five=Set rank to five -A\ string\ with\ the\ label\ '%0'\ already\ exists.=A string with the label '%0' already exists. - Executing\ command\ "%0"...=Executing command "%0"... Rename\ file\ to\ a\ given\ name=Rename file to a given name diff --git a/src/test/java/org/jabref/gui/ClipBoardManagerTest.java b/src/test/java/org/jabref/gui/ClipBoardManagerTest.java new file mode 100644 index 00000000000..087e6573b9d --- /dev/null +++ b/src/test/java/org/jabref/gui/ClipBoardManagerTest.java @@ -0,0 +1,128 @@ +package org.jabref.gui; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.input.Clipboard; + +import org.jabref.architecture.AllowedToUseAwt; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@AllowedToUseAwt("Requires AWT for clipboard access") +public class ClipBoardManagerTest { + + private BibEntryTypesManager entryTypesManager; + private ClipBoardManager clipBoardManager; + + @BeforeEach + void setUp() { + // create preference service mock + PreferencesService preferencesService = mock(PreferencesService.class); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + List fields = List.of(StandardField.URL); + ObservableList nonWrappableFields = FXCollections.observableArrayList(fields); + // set up mock behaviours for preferences service + when(fieldPreferences.getNonWrappableFields()).thenReturn(nonWrappableFields); + when(preferencesService.getFieldPreferences()).thenReturn(fieldPreferences); + + // create mock clipboard + Clipboard clipboard = mock(Clipboard.class); + // create primary clipboard and set a temporary value + StringSelection selection = new StringSelection("test"); + java.awt.datatransfer.Clipboard clipboardPrimary = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboardPrimary.setContents(selection, selection); + + // create mock entry manager and set up behaviour for mock + entryTypesManager = new BibEntryTypesManager(); + + // initialize a clipBoardManager + clipBoardManager = new ClipBoardManager(clipboard, clipboardPrimary, preferencesService); + } + + @DisplayName("Check that the ClipBoardManager can set a bibentry as its content from the clipboard") + @Test + void copyStringBibEntry() throws IOException { + // Arrange + String expected = "@Article{,\n author = {Claudepierre, S. G.},\n journal = {IEEE},\n}"; + + // create BibEntry + BibEntry bibEntry = new BibEntry(); + // construct an entry + bibEntry.setType(StandardEntryType.Article); + bibEntry.setField(StandardField.JOURNAL, "IEEE"); + bibEntry.setField(StandardField.AUTHOR, "Claudepierre, S. G."); + // add entry to list + List bibEntries = new ArrayList<>(); + bibEntries.add(bibEntry); + + // Act + clipBoardManager.setContent(bibEntries, entryTypesManager); + + // Assert + String actual = ClipBoardManager.getContentsPrimary(); + // clean strings + actual = actual.replaceAll("\\s+", " ").trim(); + expected = expected.replaceAll("\\s+", " ").trim(); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Check that the ClipBoardManager can handle a bibentry with string constants correctly from the clipboard") + void copyStringBibEntryWithStringConstants() throws IOException { + // Arrange + String expected = "@String{grl = \"Geophys. Res. Lett.\"}@Article{,\n" + " author = {Claudepierre, S. G.},\n" + + " journal = {grl},\n" + "}"; + // create BibEntry + BibEntry bibEntry = new BibEntry(); + // construct an entry + bibEntry.setType(StandardEntryType.Article); + bibEntry.setField(StandardField.JOURNAL, "grl"); + bibEntry.setField(StandardField.AUTHOR, "Claudepierre, S. G."); + // add entry to list + List bibEntries = new ArrayList<>(); + bibEntries.add(bibEntry); + + // string constants + List constants = new ArrayList<>(); + + // Mock BibtexString + BibtexString bibtexString = mock(BibtexString.class); + + // define return value for getParsedSerialization() + when(bibtexString.getParsedSerialization()).thenReturn("@String{grl = \"Geophys. Res. Lett.\"}"); + // add the constant + constants.add(bibtexString); + + // Act + clipBoardManager.setContent(bibEntries, entryTypesManager, constants); + + // Assert + String actual = ClipBoardManager.getContentsPrimary(); + // clean strings + actual = actual.replaceAll("\\s+", " ").trim(); + expected = expected.replaceAll("\\s+", " ").trim(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java b/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java index 60311301dee..d7a75a60be3 100644 --- a/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java +++ b/src/test/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesViewModelTest.java @@ -1,6 +1,7 @@ package org.jabref.gui.libraryproperties.constants; import java.util.List; +import java.util.stream.Stream; import javafx.beans.property.StringProperty; @@ -70,4 +71,65 @@ void stringsListPropertyResorting() { assertEquals(expected, actual); } + + @Test + @DisplayName("Check that the storeSettings method store settings on the model") + void storeSettingsTest() { + // Setup + // create a bibdatabse + BibDatabase db = new BibDatabase(); + BibDatabaseContext context = new BibDatabaseContext(db); + List expected = List.of("KTH", "Royal Institute of Technology"); + // initialize a constantsPropertiesViewModel + ConstantsPropertiesViewModel model = new ConstantsPropertiesViewModel(context, service, filePreferences); + + // construct value to store in model + var stringsList = model.stringsListProperty(); + stringsList.add(new ConstantsItemModel("KTH", "Royal Institute of Technology")); + + // Act + model.storeSettings(); + + // Assert + // get the names stored + List names = context.getDatabase().getStringValues().stream() + .map(BibtexString::getName).toList(); + // get the content stored + List content = context.getDatabase().getStringValues().stream() + .map(BibtexString::getContent).toList(); + + List actual = Stream.concat(names.stream(), content.stream()).toList(); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Check that the storeSettings method can identify string constants") + void storeSettingsWithStringConstantTest() { + // Setup + // create a bibdatabse + BibDatabase db = new BibDatabase(); + BibDatabaseContext context = new BibDatabaseContext(db); + List expected = List.of("@String{KTH = Royal Institute of Technology}"); + // initialize a constantsPropertiesViewModel + ConstantsPropertiesViewModel model = new ConstantsPropertiesViewModel(context, service, filePreferences); + + // construct value to store in model + var stringsList = model.stringsListProperty(); + stringsList.add(new ConstantsItemModel("KTH", "Royal Institute of Technology")); + + // Act + model.storeSettings(); + + // Assert + // get string the constants through parsedSerialization() method + List actual = context.getDatabase().getStringValues().stream() + .map(BibtexString::getParsedSerialization).toList(); + + // get the first value and clean strings + String actual_value = actual.getFirst().replaceAll("\\s+", " ").trim(); + String expected_value = expected.getFirst().replaceAll("\\s+", " ").trim(); + + assertEquals(expected_value, actual_value); + } } diff --git a/src/test/java/org/jabref/model/database/BibDatabaseTest.java b/src/test/java/org/jabref/model/database/BibDatabaseTest.java index 28e9db06298..e904015354a 100644 --- a/src/test/java/org/jabref/model/database/BibDatabaseTest.java +++ b/src/test/java/org/jabref/model/database/BibDatabaseTest.java @@ -392,8 +392,8 @@ void resolveForStringsOddHashMarkAtTheEnd() { @Test void getUsedStrings() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.AUTHOR, "#AAA#"); + BibEntry entry = new BibEntry() + .withField(StandardField.AUTHOR, "#AAA#"); BibtexString tripleA = new BibtexString("AAA", "Some other #BBB#"); BibtexString tripleB = new BibtexString("BBB", "Some more text"); BibtexString tripleC = new BibtexString("CCC", "Even more text"); @@ -412,8 +412,8 @@ void getUsedStrings() { @Test void getUsedStringsSingleString() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.AUTHOR, "#AAA#"); + BibEntry entry = new BibEntry() + .withField(StandardField.AUTHOR, "#AAA#"); BibtexString tripleA = new BibtexString("AAA", "Some other text"); BibtexString tripleB = new BibtexString("BBB", "Some more text"); List strings = new ArrayList<>(1); @@ -429,8 +429,8 @@ void getUsedStringsSingleString() { @Test void getUsedStringsNoString() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.AUTHOR, "Oscar Gustafsson"); + BibEntry entry = new BibEntry() + .withField(StandardField.AUTHOR, "Oscar Gustafsson"); BibtexString string = new BibtexString("AAA", "Some other text"); database.addString(string); database.insertEntry(entry); From 95875204bec423a15e77019ed802e5fe5fc5d219 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:32:18 +0000 Subject: [PATCH 12/38] Bump gittools/actions from 0.13.4 to 1.1.1 (#11039) Bumps [gittools/actions](https://github.com/gittools/actions) from 0.13.4 to 1.1.1. - [Release notes](https://github.com/gittools/actions/releases) - [Commits](https://github.com/gittools/actions/compare/v0.13.4...v1.1.1) --- updated-dependencies: - dependency-name: gittools/actions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deployment-arm64.yml | 4 ++-- .github/workflows/deployment-jdk-ea.yml | 4 ++-- .github/workflows/deployment.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deployment-arm64.yml b/.github/workflows/deployment-arm64.yml index a505062a065..bad858dc0a1 100644 --- a/.github/workflows/deployment-arm64.yml +++ b/.github/workflows/deployment-arm64.yml @@ -66,12 +66,12 @@ jobs: submodules: 'true' show-progress: 'false' - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.13.4 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: "5.x" - name: Run GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.13.4 + uses: gittools/actions/gitversion/execute@v1.1.1 - name: Setup JDK uses: actions/setup-java@v4 with: diff --git a/.github/workflows/deployment-jdk-ea.yml b/.github/workflows/deployment-jdk-ea.yml index 20c018e7968..620e0a8f639 100644 --- a/.github/workflows/deployment-jdk-ea.yml +++ b/.github/workflows/deployment-jdk-ea.yml @@ -87,12 +87,12 @@ jobs: packages: pigz version: 1.0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.13.4 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: "5.x" - name: Run GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.13.4 + uses: gittools/actions/gitversion/execute@v1.1.1 - name: 'Set up JDK ${{ matrix.jdk }}' uses: oracle-actions/setup-java@v1 with: diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index a18391321ed..6a4b60e4bd2 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -79,12 +79,12 @@ jobs: packages: pigz version: 1.0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.13.4 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: "5.x" - name: Run GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.13.4 + uses: gittools/actions/gitversion/execute@v1.1.1 - name: Setup JDK uses: actions/setup-java@v4 with: From 1ec6a6ea06ef9a3ed1f3237a85997abcc35b6a73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:46:41 +0000 Subject: [PATCH 13/38] Bump com.googlecode.plist:dd-plist from 1.23 to 1.28 (#11040) Bumps [com.googlecode.plist:dd-plist](https://github.com/3breadt/dd-plist) from 1.23 to 1.28. - [Release notes](https://github.com/3breadt/dd-plist/releases) - [Commits](https://github.com/3breadt/dd-plist/compare/dd-plist-1.23...v1.28.0) --- updated-dependencies: - dependency-name: com.googlecode.plist:dd-plist dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 61eeaafc33f..7c3bfb8d11a 100644 --- a/build.gradle +++ b/build.gradle @@ -243,7 +243,7 @@ dependencies { implementation 'org.jspecify:jspecify:0.3.0' // parse plist files - implementation 'com.googlecode.plist:dd-plist:1.23' + implementation 'com.googlecode.plist:dd-plist:1.28' testImplementation 'io.github.classgraph:classgraph:4.8.168' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' From 930a9b42b8c2941ca72b4fd48b6e3a59c66eaa5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:47:28 +0000 Subject: [PATCH 14/38] Bump org.apache.pdfbox:xmpbox from 3.0.1 to 3.0.2 (#11041) Bumps org.apache.pdfbox:xmpbox from 3.0.1 to 3.0.2. --- updated-dependencies: - dependency-name: org.apache.pdfbox:xmpbox dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7c3bfb8d11a..4c87397f740 100644 --- a/build.gradle +++ b/build.gradle @@ -115,7 +115,7 @@ dependencies { implementation 'org.apache.pdfbox:pdfbox:3.0.1' implementation 'org.apache.pdfbox:fontbox:3.0.1' - implementation ('org.apache.pdfbox:xmpbox:3.0.1') { + implementation ('org.apache.pdfbox:xmpbox:3.0.2') { exclude group: 'org.junit.jupiter' } From 5858598dcb88bd6e07e3300bd8085343133ba31e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:57:12 +0000 Subject: [PATCH 15/38] Bump com.dlsc.gemsfx:gemsfx from 2.2.0 to 2.4.0 (#11044) Bumps [com.dlsc.gemsfx:gemsfx](https://github.com/dlsc-software-consulting-gmbh/GemsFX) from 2.2.0 to 2.4.0. - [Release notes](https://github.com/dlsc-software-consulting-gmbh/GemsFX/releases) - [Changelog](https://github.com/dlsc-software-consulting-gmbh/GemsFX/blob/master/CHANGELOG.md) - [Commits](https://github.com/dlsc-software-consulting-gmbh/GemsFX/compare/v2.2.0...v2.4.0) --- updated-dependencies: - dependency-name: com.dlsc.gemsfx:gemsfx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c87397f740..8f36f12674c 100644 --- a/build.gradle +++ b/build.gradle @@ -177,7 +177,7 @@ dependencies { implementation('com.tobiasdiez:easybind:2.2.1-SNAPSHOT') implementation 'org.fxmisc.flowless:flowless:0.7.2' implementation 'org.fxmisc.richtext:richtextfx:0.11.2' - implementation (group: 'com.dlsc.gemsfx', name: 'gemsfx', version: '2.2.0') { + implementation (group: 'com.dlsc.gemsfx', name: 'gemsfx', version: '2.4.0') { exclude module: 'javax.inject' // Split package, use only jakarta.inject exclude group: 'org.apache.logging.log4j' } From 7cb8885e0b4af137650bb713257d06163f470411 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:21:23 +0000 Subject: [PATCH 16/38] Bump org.apache.pdfbox:fontbox from 3.0.1 to 3.0.2 (#11042) Bumps org.apache.pdfbox:fontbox from 3.0.1 to 3.0.2. --- updated-dependencies: - dependency-name: org.apache.pdfbox:fontbox dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8f36f12674c..666c35f2336 100644 --- a/build.gradle +++ b/build.gradle @@ -114,7 +114,7 @@ dependencies { implementation fileTree(dir: 'lib', includes: ['*.jar']) implementation 'org.apache.pdfbox:pdfbox:3.0.1' - implementation 'org.apache.pdfbox:fontbox:3.0.1' + implementation 'org.apache.pdfbox:fontbox:3.0.2' implementation ('org.apache.pdfbox:xmpbox:3.0.2') { exclude group: 'org.junit.jupiter' } From 342cb2419d434ca29eafdcf8818b0203cea1a2e1 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 18 Mar 2024 17:57:07 +0100 Subject: [PATCH 17/38] Keep enclosing braces of authors (#11034) * Add test cases * Add test cases * Keep braces for last part * Refine method description * Adapt test to new braces keeping * Add CHANGELOG.md entry * Adapt tests * More edge cases * Minor code beautification * Simplify code * Fix braces removing * Extract static fields, refactor code * Fix removal of {} for export * Re-add Objects.requireNonNull * Fix typo * Re-add NPE throwing * Rename to modern terms * Consistent initialization --- CHANGELOG.md | 1 + .../PersonNameStringConverter.java | 14 +- .../PersonNameSuggestionProvider.java | 4 +- .../logic/bst/util/BstNameFormatter.java | 12 +- .../citationkeypattern/BracketedPattern.java | 38 ++--- .../jabref/logic/exporter/MSBibExporter.java | 18 ++- .../bibtexfields/HtmlToLatexFormatter.java | 3 + .../bibtexfields/RemoveBracesFormatter.java | 9 +- .../logic/importer/AuthorListParser.java | 7 +- .../org/jabref/logic/importer/Importer.java | 2 +- .../jabref/logic/importer/fetcher/ZbMATH.java | 2 +- .../importer/fileformat/MsBibImporter.java | 23 +-- .../logic/layout/format/AuthorOrgSci.java | 4 +- .../jabref/logic/layout/format/Authors.java | 14 +- .../layout/format/DocBookAuthorFormatter.java | 12 +- .../jabref/logic/msbib/BibTeXConverter.java | 3 - .../jabref/logic/msbib/MSBibConverter.java | 32 +++-- .../org/jabref/logic/msbib/MSBibDatabase.java | 41 +++--- .../org/jabref/logic/msbib/MSBibEntry.java | 11 +- .../org/jabref/logic/msbib/MSBibMapping.java | 78 +++++----- .../org/jabref/logic/msbib/MsBibAuthor.java | 13 +- .../style/OOBibStyleGetCitationMarker.java | 32 ++--- .../jabref/logic/xmp/DublinCoreExtractor.java | 4 +- .../java/org/jabref/model/entry/Author.java | 136 ++++++++++-------- .../org/jabref/model/entry/AuthorList.java | 39 +++-- .../jabref/model/groups/LastNameGroup.java | 2 +- .../exporter/MSBibExportFormatFilesTest.java | 7 +- .../NormalizeNamesFormatterTest.java | 3 + .../logic/importer/AuthorListParserTest.java | 9 +- .../logic/msbib/MSBibConverterTest.java | 10 +- .../jabref/logic/msbib/MsBibAuthorTest.java | 6 +- .../openoffice/style/OOBibStyleTest.java | 18 +-- .../style/OOBibStyleTestHelper.java | 6 +- .../openoffice/style/OOPreFormatterTest.java | 5 + .../jabref/model/entry/AuthorListTest.java | 76 +++++----- .../org/jabref/model/entry/AuthorTest.java | 7 + 36 files changed, 358 insertions(+), 343 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a4d245079d..0334426d200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We made the command "Push to TexShop" more robust to allow cite commands with a character before the first slash. [forum#2699](https://discourse.jabref.org/t/push-to-texshop-mac/2699/17?u=siedlerchr) - We only show the notification "Saving library..." if the library contains more than 2000 entries. [#9803](https://github.com/JabRef/jabref/issues/9803) - JabRef now keeps previous log files upon start. [#11023](https://github.com/JabRef/jabref/pull/11023) +- When normalizing author names, complete enclosing braces are kept. [#10031](https://github.com/JabRef/jabref/issues/10031) - We enhanced the dialog for adding new fields in the content selector with a selection box containing a list of standard fields. [#10912](https://github.com/JabRef/jabref/pull/10912) - 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) diff --git a/src/main/java/org/jabref/gui/autocompleter/PersonNameStringConverter.java b/src/main/java/org/jabref/gui/autocompleter/PersonNameStringConverter.java index c78312638af..f883def6487 100644 --- a/src/main/java/org/jabref/gui/autocompleter/PersonNameStringConverter.java +++ b/src/main/java/org/jabref/gui/autocompleter/PersonNameStringConverter.java @@ -42,11 +42,11 @@ public String toString(Author author) { if (autoCompLF) { switch (autoCompleteFirstNameMode) { case ONLY_ABBREVIATED: - return author.getLastFirst(true); + return author.getFamilyGiven(true); case ONLY_FULL: - return author.getLastFirst(false); + return author.getFamilyGiven(false); case BOTH: - return author.getLastFirst(true); + return author.getFamilyGiven(true); default: break; } @@ -54,16 +54,16 @@ public String toString(Author author) { if (autoCompFF) { switch (autoCompleteFirstNameMode) { case ONLY_ABBREVIATED: - return author.getFirstLast(true); + return author.getGivenFamily(true); case ONLY_FULL: - return author.getFirstLast(false); + return author.getGivenFamily(false); case BOTH: - return author.getFirstLast(true); + return author.getGivenFamily(true); default: break; } } - return author.getLastOnly(); + return author.getNamePrefixAndFamilyName(); } @Override diff --git a/src/main/java/org/jabref/gui/autocompleter/PersonNameSuggestionProvider.java b/src/main/java/org/jabref/gui/autocompleter/PersonNameSuggestionProvider.java index 2ff7d4a4ed1..8457ea5dbd5 100644 --- a/src/main/java/org/jabref/gui/autocompleter/PersonNameSuggestionProvider.java +++ b/src/main/java/org/jabref/gui/autocompleter/PersonNameSuggestionProvider.java @@ -48,7 +48,7 @@ public Stream getAuthors(BibEntry entry) { @Override protected Equivalence getEquivalence() { - return Equivalence.equals().onResultOf(Author::getLastOnly); + return Equivalence.equals().onResultOf(Author::getNamePrefixAndFamilyName); } @Override @@ -58,7 +58,7 @@ protected Comparator getComparator() { @Override protected boolean isMatch(Author candidate, AutoCompletionBinding.ISuggestionRequest request) { - return StringUtil.containsIgnoreCase(candidate.getLastFirst(false), request.getUserText()); + return StringUtil.containsIgnoreCase(candidate.getFamilyGiven(false), request.getUserText()); } @Override diff --git a/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java index cd65735aa57..a49b1527a8d 100644 --- a/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java +++ b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java @@ -102,14 +102,10 @@ public static String formatName(Author author, String format) { char type = control.charAt(0); Optional tokenS = switch (type) { - case 'f' -> - author.getFirst(); - case 'v' -> - author.getVon(); - case 'l' -> - author.getLast(); - case 'j' -> - author.getJr(); + case 'f' -> author.getGivenName(); + case 'v' -> author.getNamePrefix(); + case 'l' -> author.getFamilyName(); + case 'j' -> author.getNameSuffix(); default -> throw new BstVMException("Internal error"); }; diff --git a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java index 6d8206dc02e..68b378c2cfe 100644 --- a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java +++ b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java @@ -526,17 +526,17 @@ private static AuthorList createAuthorList(String unparsedAuthors) { return AuthorList.parse(unparsedAuthors).getAuthors().stream() .map(author -> { // If the author is an institution, use an institution key instead of the full name - String lastName = author.getLast() + String lastName = author.getFamilyName() .map(lastPart -> isInstitution(author) ? generateInstitutionKey(lastPart) : LatexToUnicodeAdapter.format(lastPart)) .orElse(null); return new Author( - author.getFirst().map(LatexToUnicodeAdapter::format).orElse(null), - author.getFirstAbbr().map(LatexToUnicodeAdapter::format).orElse(null), - author.getVon().map(LatexToUnicodeAdapter::format).orElse(null), + author.getGivenName().map(LatexToUnicodeAdapter::format).orElse(null), + author.getGivenNameAbbreviated().map(LatexToUnicodeAdapter::format).orElse(null), + author.getNamePrefix().map(LatexToUnicodeAdapter::format).orElse(null), lastName, - author.getJr().map(LatexToUnicodeAdapter::format).orElse(null)); + author.getNameSuffix().map(LatexToUnicodeAdapter::format).orElse(null)); }) .collect(AuthorList.collect()); } @@ -548,9 +548,9 @@ private static AuthorList createAuthorList(String unparsedAuthors) { * @return true if only the last name is present and it contains at least one whitespace character. */ private static boolean isInstitution(Author author) { - return author.getFirst().isEmpty() && author.getFirstAbbr().isEmpty() && author.getJr().isEmpty() - && author.getVon().isEmpty() && author.getLast().isPresent() - && WHITESPACE.matcher(author.getLast().get()).find(); + return author.getGivenName().isEmpty() && author.getGivenNameAbbreviated().isEmpty() && author.getNameSuffix().isEmpty() + && author.getNamePrefix().isEmpty() && author.getFamilyName().isPresent() + && WHITESPACE.matcher(author.getFamilyName().get()).find(); } /** @@ -765,7 +765,7 @@ private static String keepLettersAndDigitsOnly(String in) { private static String firstAuthor(AuthorList authorList) { return authorList.getAuthors().stream() .findFirst() - .flatMap(author -> author.getLast().isPresent() ? author.getLast() : author.getVon()) + .flatMap(author -> author.getFamilyName().isPresent() ? author.getFamilyName() : author.getNamePrefix()) .orElse(""); } @@ -779,7 +779,7 @@ private static String firstAuthor(AuthorList authorList) { private static String firstAuthorForenameInitials(AuthorList authorList) { return authorList.getAuthors().stream() .findFirst() - .flatMap(Author::getFirstAbbr) + .flatMap(Author::getGivenNameAbbreviated) .map(s -> s.substring(0, 1)) .orElse(""); } @@ -793,7 +793,7 @@ private static String firstAuthorForenameInitials(AuthorList authorList) { */ private static String firstAuthorVonAndLast(AuthorList authorList) { return authorList.isEmpty() ? "" : - authorList.getAuthor(0).getLastOnly().replace(" ", ""); + authorList.getAuthor(0).getNamePrefixAndFamilyName().replace(" ", ""); } /** @@ -806,7 +806,7 @@ private static String lastAuthor(AuthorList authorList) { if (authorList.isEmpty()) { return ""; } - return authorList.getAuthors().get(authorList.getNumberOfAuthors() - 1).getLast().orElse(""); + return authorList.getAuthors().get(authorList.getNumberOfAuthors() - 1).getFamilyName().orElse(""); } /** @@ -820,7 +820,7 @@ private static String lastAuthorForenameInitials(AuthorList authorList) { if (authorList.isEmpty()) { return ""; } - return authorList.getAuthor(authorList.getNumberOfAuthors() - 1).getFirstAbbr().map(s -> s.substring(0, 1)) + return authorList.getAuthor(authorList.getNumberOfAuthors() - 1).getGivenNameAbbreviated().map(s -> s.substring(0, 1)) .orElse(""); } @@ -857,7 +857,7 @@ static String authorsAlpha(AuthorList authorList) { } if (authorList.getNumberOfAuthors() == 1) { - String[] firstAuthor = authorList.getAuthor(0).getLastOnly() + String[] firstAuthor = authorList.getAuthor(0).getNamePrefixAndFamilyName() .replaceAll("\\s+", " ").trim().split(" "); // take first letter of any "prefixes" (e.g. van der Aalst -> vd) for (int j = 0; j < (firstAuthor.length - 1); j++) { @@ -873,7 +873,7 @@ static String authorsAlpha(AuthorList authorList) { } List vonAndLastNames = authorList.getAuthors().stream() .limit(maxAuthors) - .map(Author::getLastOnly) + .map(Author::getNamePrefixAndFamilyName) .collect(Collectors.toList()); for (String vonAndLast : vonAndLastNames) { // replace all whitespaces by " " @@ -913,7 +913,7 @@ private static String joinAuthorsOnLastName(AuthorList authorList, int maxAuthor return Optional.of(suffix); } } else { - return author.getLast(); + return author.getFamilyName(); } }) .flatMap(Optional::stream) @@ -970,7 +970,7 @@ static String authEtal(AuthorList authorList, String delim, String append) { // exception: If the second author is "and others", then do the appendix handling (in the other branch) return joinAuthorsOnLastName(authorList, 2, delim, ""); } else { - return authorList.getAuthor(0).getLast().orElse("") + append; + return authorList.getAuthor(0).getFamilyName().orElse("") + append; } } @@ -990,7 +990,7 @@ private static String authNofMth(AuthorList authorList, int n, int m) { if (lastAuthor.equals(Author.OTHERS)) { return "+"; } - String lastName = lastAuthor.getLast() + String lastName = lastAuthor.getFamilyName() .map(CitationKeyGenerator::removeDefaultUnwantedCharacters).orElse(""); return lastName.length() > n ? lastName.substring(0, n) : lastName; } @@ -1010,7 +1010,7 @@ static String authShort(AuthorList authorList) { final int numberOfAuthors = authorList.getNumberOfAuthors(); if (numberOfAuthors == 1) { - author.append(authorList.getAuthor(0).getLast().orElse("")); + author.append(authorList.getAuthor(0).getFamilyName().orElse("")); } else if (numberOfAuthors >= 2) { for (int i = 0; (i < numberOfAuthors) && (i < 3); i++) { author.append(authNofMth(authorList, 1, i + 1)); diff --git a/src/main/java/org/jabref/logic/exporter/MSBibExporter.java b/src/main/java/org/jabref/logic/exporter/MSBibExporter.java index 1a1da69dab1..ea80cefc7f4 100644 --- a/src/main/java/org/jabref/logic/exporter/MSBibExporter.java +++ b/src/main/java/org/jabref/logic/exporter/MSBibExporter.java @@ -19,33 +19,37 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jspecify.annotations.NonNull; + /** * TemplateExporter for exporting in MSBIB XML format. */ class MSBibExporter extends Exporter { + private final TransformerFactory transformerFactory; + public MSBibExporter() { super("MSBib", "MS Office 2007", StandardFileType.XML); + transformerFactory = TransformerFactory.newInstance(); } @Override - public void export(final BibDatabaseContext databaseContext, final Path file, - List entries) throws SaveException { - Objects.requireNonNull(databaseContext); - Objects.requireNonNull(entries); - + public void export(@NonNull BibDatabaseContext databaseContext, + @NonNull Path file, + @NonNull List entries) throws SaveException { + Objects.requireNonNull(databaseContext); // required by test case if (entries.isEmpty()) { return; } MSBibDatabase msBibDatabase = new MSBibDatabase(databaseContext.getDatabase(), entries); - // forcing to use UTF8 output format for some problems with xml export in other encodings + // forcing to use UTF8 output format for some problems with XML export in other encodings try (AtomicFileWriter ps = new AtomicFileWriter(file, StandardCharsets.UTF_8)) { try { DOMSource source = new DOMSource(msBibDatabase.getDomForExport()); StreamResult result = new StreamResult(ps); - Transformer trans = TransformerFactory.newInstance().newTransformer(); + Transformer trans = transformerFactory.newTransformer(); trans.setOutputProperty(OutputKeys.INDENT, "yes"); trans.transform(source, result); } catch (TransformerException | IllegalArgumentException | TransformerFactoryConfigurationError e) { diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/HtmlToLatexFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/HtmlToLatexFormatter.java index 33664983425..a9db09f1e4f 100644 --- a/src/main/java/org/jabref/logic/formatter/bibtexfields/HtmlToLatexFormatter.java +++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/HtmlToLatexFormatter.java @@ -13,6 +13,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * The inverse operation is "somehow" contained in {@link org.jabref.logic.openoffice.style.OOPreFormatter} + */ public class HtmlToLatexFormatter extends Formatter implements LayoutFormatter { private static final Logger LOGGER = LoggerFactory.getLogger(HtmlToLatexFormatter.class); diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatter.java index fdbc4f21d09..c29b081cc48 100644 --- a/src/main/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatter.java +++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/RemoveBracesFormatter.java @@ -1,10 +1,11 @@ package org.jabref.logic.formatter.bibtexfields; -import java.util.Objects; - import org.jabref.logic.cleanup.Formatter; import org.jabref.logic.l10n.Localization; +import org.jspecify.annotations.NullMarked; + +@NullMarked public class RemoveBracesFormatter extends Formatter { @Override @@ -19,8 +20,6 @@ public String getKey() { @Override public String format(String value) { - Objects.requireNonNull(value); - String formatted = value; while ((formatted.length() >= 2) && (formatted.charAt(0) == '{') && (formatted.charAt(formatted.length() - 1) == '}')) { @@ -50,7 +49,7 @@ public String getExampleInput() { /** * Check if a string at any point has had more ending } braces than opening { ones. - * Will e.g. return true for the string "DNA} blahblal {EPA" + * Will e.g. return true for the string "DNA} text {EPA" * * @param value The string to check. * @return true if at any index the brace count is negative. diff --git a/src/main/java/org/jabref/logic/importer/AuthorListParser.java b/src/main/java/org/jabref/logic/importer/AuthorListParser.java index 874ec1525ad..6ef699d3db1 100644 --- a/src/main/java/org/jabref/logic/importer/AuthorListParser.java +++ b/src/main/java/org/jabref/logic/importer/AuthorListParser.java @@ -6,7 +6,6 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -14,6 +13,8 @@ import org.jabref.model.entry.AuthorList; import org.jabref.model.strings.StringUtil; +import org.jspecify.annotations.NonNull; + public class AuthorListParser { // Avoid partition where these values are contained @@ -94,9 +95,7 @@ private static StringBuilder buildWithAffix(Collection indexArray, List * @param listOfNames the String containing the person names to be parsed * @return a parsed list of persons */ - public AuthorList parse(String listOfNames) { - Objects.requireNonNull(listOfNames); - + public AuthorList parse(@NonNull String listOfNames) { // Handling of "and others" // Remove it from the list; it will be added at the very end of this method as special Author.OTHERS listOfNames = listOfNames.trim(); diff --git a/src/main/java/org/jabref/logic/importer/Importer.java b/src/main/java/org/jabref/logic/importer/Importer.java index d2a8a24ec36..fa4159dc404 100644 --- a/src/main/java/org/jabref/logic/importer/Importer.java +++ b/src/main/java/org/jabref/logic/importer/Importer.java @@ -36,7 +36,7 @@ public abstract class Importer implements Comparable { * The effect of this method is primarily to avoid unnecessary processing of files when searching for a suitable * import format. If this method returns false, the import routine will move on to the next import format. *

- * Thus the correct behaviour is to return false if it is certain that the file is not of the suitable type, and + * Thus, the correct behaviour is to return false if it is certain that the file is not of the suitable type, and * true otherwise. Returning true is the safe choice if not certain. */ public abstract boolean isRecognizedFormat(BufferedReader input) throws IOException; diff --git a/src/main/java/org/jabref/logic/importer/fetcher/ZbMATH.java b/src/main/java/org/jabref/logic/importer/fetcher/ZbMATH.java index f1b0b5fb32c..70afa7847c1 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/ZbMATH.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/ZbMATH.java @@ -71,7 +71,7 @@ public URL getURLForEntry(BibEntry entry) throws URISyntaxException, MalformedUR // replace "and" by ";" as citation matching API uses ";" for separation AuthorList authors = AuthorList.parse(entry.getFieldOrAlias(StandardField.AUTHOR).get()); String authorsWithSemicolon = authors.getAuthors().stream() - .map(author -> author.getLastFirst(false)) + .map(author -> author.getFamilyGiven(false)) .collect(Collectors.joining(";")); uriBuilder.addParameter("a", authorsWithSemicolon); } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MsBibImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MsBibImporter.java index 31863a621eb..f162f5e9c84 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MsBibImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MsBibImporter.java @@ -13,6 +13,7 @@ import org.jabref.logic.msbib.MSBibDatabase; import org.jabref.logic.util.StandardFileType; +import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -24,24 +25,25 @@ /** * Importer for the MS Office 2007 XML bibliography format */ +@NullMarked public class MsBibImporter extends Importer { private static final Logger LOGGER = LoggerFactory.getLogger(MsBibImporter.class); private static final String DISABLEDTD = "http://apache.org/xml/features/disallow-doctype-decl"; private static final String DISABLEEXTERNALDTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = makeSafeDocBuilderFactory(DocumentBuilderFactory.newInstance()); + /** + * The correct behavior is to return false if it is certain that the file is + * not of the MsBib type, and true otherwise. Returning true is the safe choice + * if not certain. + */ @Override public boolean isRecognizedFormat(BufferedReader reader) throws IOException { - Objects.requireNonNull(reader); - - /* - The correct behavior is to return false if it is certain that the file is - not of the MsBib type, and true otherwise. Returning true is the safe choice - if not certain. - */ + Objects.requireNonNull(reader); // Required by test case Document docin; try { - DocumentBuilder dbuild = makeSafeDocBuilderFactory(DocumentBuilderFactory.newInstance()).newDocumentBuilder(); + DocumentBuilder dbuild = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); dbuild.setErrorHandler(new ErrorHandler() { @Override public void warning(SAXParseException exception) throws SAXException { @@ -68,8 +70,7 @@ public void error(SAXParseException exception) throws SAXException { @Override public ParserResult importDatabase(BufferedReader reader) throws IOException { - Objects.requireNonNull(reader); - + Objects.requireNonNull(reader); // Required by test case MSBibDatabase dbase = new MSBibDatabase(); return new ParserResult(dbase.importEntriesFromXml(reader)); } @@ -96,7 +97,7 @@ public String getDescription() { * @param dBuild | DocumentBuilderFactory to be made XXE safe. * @return If supported, XXE safe DocumentBuilderFactory. Else, returns original builder given */ - private DocumentBuilderFactory makeSafeDocBuilderFactory(DocumentBuilderFactory dBuild) { + private static DocumentBuilderFactory makeSafeDocBuilderFactory(DocumentBuilderFactory dBuild) { String feature = null; try { diff --git a/src/main/java/org/jabref/logic/layout/format/AuthorOrgSci.java b/src/main/java/org/jabref/logic/layout/format/AuthorOrgSci.java index 4671220df73..75bc76821f4 100644 --- a/src/main/java/org/jabref/logic/layout/format/AuthorOrgSci.java +++ b/src/main/java/org/jabref/logic/layout/format/AuthorOrgSci.java @@ -30,9 +30,9 @@ public String format(String fieldText) { } Author first = a.getAuthor(0); StringBuilder sb = new StringBuilder(); - sb.append(first.getLastFirst(true)); + sb.append(first.getFamilyGiven(true)); for (int i = 1; i < a.getNumberOfAuthors(); i++) { - sb.append(", ").append(a.getAuthor(i).getFirstLast(true)); + sb.append(", ").append(a.getAuthor(i).getGivenFamily(true)); } return sb.toString(); } diff --git a/src/main/java/org/jabref/logic/layout/format/Authors.java b/src/main/java/org/jabref/logic/layout/format/Authors.java index 5692470fac3..dd6bef7ad54 100644 --- a/src/main/java/org/jabref/logic/layout/format/Authors.java +++ b/src/main/java/org/jabref/logic/layout/format/Authors.java @@ -254,21 +254,21 @@ public String format(String fieldText) { private void addSingleName(StringBuilder sb, Author a, boolean firstFirst) { StringBuilder lastNameSB = new StringBuilder(); - a.getVon().filter(von -> !von.isEmpty()).ifPresent(von -> lastNameSB.append(von).append(' ')); - a.getLast().ifPresent(lastNameSB::append); + a.getNamePrefix().filter(von -> !von.isEmpty()).ifPresent(von -> lastNameSB.append(von).append(' ')); + a.getFamilyName().ifPresent(lastNameSB::append); String jrSeparator = " "; - a.getJr().filter(jr -> !jr.isEmpty()).ifPresent(jr -> lastNameSB.append(jrSeparator).append(jr)); + a.getNameSuffix().filter(jr -> !jr.isEmpty()).ifPresent(jr -> lastNameSB.append(jrSeparator).append(jr)); String firstNameResult = ""; - if (a.getFirst().isPresent()) { + if (a.getGivenName().isPresent()) { if (abbreviate) { - firstNameResult = a.getFirstAbbr().orElse(""); + firstNameResult = a.getGivenNameAbbreviated().orElse(""); if (firstInitialOnly && (firstNameResult.length() > 2)) { firstNameResult = firstNameResult.substring(0, 2); } else if (middleInitial) { String abbr = firstNameResult; - firstNameResult = a.getFirst().get(); + firstNameResult = a.getGivenName().get(); int index = firstNameResult.indexOf(' '); if (index >= 0) { firstNameResult = firstNameResult.substring(0, index + 1); @@ -284,7 +284,7 @@ private void addSingleName(StringBuilder sb, Author a, boolean firstFirst) { firstNameResult = firstNameResult.replace(" ", ""); } } else { - firstNameResult = a.getFirst().get(); + firstNameResult = a.getGivenName().get(); } } diff --git a/src/main/java/org/jabref/logic/layout/format/DocBookAuthorFormatter.java b/src/main/java/org/jabref/logic/layout/format/DocBookAuthorFormatter.java index 01bdfe29c46..9d1b426eb81 100644 --- a/src/main/java/org/jabref/logic/layout/format/DocBookAuthorFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/DocBookAuthorFormatter.java @@ -20,13 +20,13 @@ public void addBody(StringBuilder sb, AuthorList al, String tagName, DocBookVers sb.append(""); } Author a = al.getAuthor(i); - a.getFirst().filter(first -> !first.isEmpty()).ifPresent(first -> sb.append("") - .append(XML_CHARS.format(first)).append("")); - a.getVon().filter(von -> !von.isEmpty()).ifPresent(von -> sb.append("") - .append(XML_CHARS.format(von)).append("")); - a.getLast().filter(last -> !last.isEmpty()).ifPresent(last -> { + a.getGivenName().filter(first -> !first.isEmpty()).ifPresent(first -> sb.append("") + .append(XML_CHARS.format(first)).append("")); + a.getNamePrefix().filter(von -> !von.isEmpty()).ifPresent(von -> sb.append("") + .append(XML_CHARS.format(von)).append("")); + a.getFamilyName().filter(last -> !last.isEmpty()).ifPresent(last -> { sb.append("").append(XML_CHARS.format(last)); - a.getJr().filter(jr -> !jr.isEmpty()) + a.getNameSuffix().filter(jr -> !jr.isEmpty()) .ifPresent(jr -> sb.append(' ').append(XML_CHARS.format(jr))); sb.append(""); if (version == DocBookVersion.DOCBOOK_5) { diff --git a/src/main/java/org/jabref/logic/msbib/BibTeXConverter.java b/src/main/java/org/jabref/logic/msbib/BibTeXConverter.java index 6881252d8d0..39d5af3a42b 100644 --- a/src/main/java/org/jabref/logic/msbib/BibTeXConverter.java +++ b/src/main/java/org/jabref/logic/msbib/BibTeXConverter.java @@ -24,9 +24,6 @@ private BibTeXConverter() { /** * Converts an {@link MSBibEntry} to a {@link BibEntry} for import - * - * @param entry The MsBibEntry to convert - * @return The bib entry */ public static BibEntry convert(MSBibEntry entry) { BibEntry result; diff --git a/src/main/java/org/jabref/logic/msbib/MSBibConverter.java b/src/main/java/org/jabref/logic/msbib/MSBibConverter.java index 6812d1033de..d565195e6fc 100644 --- a/src/main/java/org/jabref/logic/msbib/MSBibConverter.java +++ b/src/main/java/org/jabref/logic/msbib/MSBibConverter.java @@ -2,8 +2,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; -import org.jabref.model.entry.Author; +import org.jabref.logic.formatter.bibtexfields.RemoveBracesFormatter; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.Month; @@ -17,6 +18,7 @@ public class MSBibConverter { private static final String MSBIB_PREFIX = "msbib-"; private static final String BIBTEX_PREFIX = "BIBTEX_"; + private static final RemoveBracesFormatter REMOVE_BRACES_FORMATTER = new RemoveBracesFormatter(); private MSBibConverter() { } @@ -123,24 +125,26 @@ public static MSBibEntry convert(BibEntry entry) { private static List getAuthors(BibEntry entry, String authors, Field field) { List result = new ArrayList<>(); - boolean corporate = false; + // Only one corporate author is supported - // We have the possible rare case that are multiple authors which start and end with latex , this is currently not considered - if (authors.startsWith("{") && authors.endsWith("}")) { - corporate = true; + // Heuristics: If the author is surrounded by curly braces, it is a corporate author + boolean corporate = !REMOVE_BRACES_FORMATTER.format(authors).equals(authors); + + Optional authorLatexFreeOpt = entry.getFieldLatexFree(field); + if (authorLatexFreeOpt.isEmpty()) { + return result; } - // FIXME: #4152 This is an ugly hack because the latex2unicode formatter kills of all curly braces, so no more corporate author parsing possible - String authorLatexFree = entry.getFieldLatexFree(field).orElse(""); + String authorLatexFree = authorLatexFreeOpt.get(); + + // We re-add the curly braces to keep the corporate author as is. + // See https://github.com/JabRef/jabref-issue-melting-pot/issues/386 for details if (corporate) { authorLatexFree = "{" + authorLatexFree + "}"; } - AuthorList authorList = AuthorList.parse(authorLatexFree); - - for (Author author : authorList.getAuthors()) { - result.add(new MsBibAuthor(author, corporate)); - } - - return result; + return AuthorList.parse(authorLatexFree).getAuthors() + .stream() + .map(author -> new MsBibAuthor(author, corporate)) + .toList(); } } diff --git a/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java b/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java index f2040674fef..10a646f401d 100644 --- a/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java +++ b/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java @@ -36,44 +36,46 @@ public class MSBibDatabase { private static final Logger LOGGER = LoggerFactory.getLogger(MSBibDatabase.class); - private Set entries; + private final DocumentBuilderFactory factory; + + private Set entriesForExport; /** * Creates a {@link MSBibDatabase} for import */ public MSBibDatabase() { - entries = new HashSet<>(); + entriesForExport = new HashSet<>(); + factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); } - // TODO: why an additonal entry list? entries are included inside database! - /** - * Creates a new {@link MSBibDatabase} for export + * Creates a new {@link MSBibDatabase} for export. + * Directly converts the given entries. * * @param database The bib database * @param entries List of {@link BibEntry} */ public MSBibDatabase(BibDatabase database, List entries) { + this(); if (entries == null) { var resolvedEntries = database.resolveForStrings(database.getEntries(), false); - addEntriesForExport(resolvedEntries); + setEntriesForExport(resolvedEntries); } else { var resolvedEntries = database.resolveForStrings(entries, false); - addEntriesForExport(resolvedEntries); + setEntriesForExport(resolvedEntries); } } /** - * Imports entries from an office xml file + * Imports entries from an office XML file * * @return List of {@link BibEntry} */ public List importEntriesFromXml(BufferedReader reader) { - entries = new HashSet<>(); + entriesForExport = new HashSet<>(); Document inputDocument; try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); DocumentBuilder documentBuilder = factory.newDocumentBuilder(); inputDocument = documentBuilder.parse(new InputSource(reader)); } catch (ParserConfigurationException | SAXException | IOException e) { @@ -92,31 +94,24 @@ public List importEntriesFromXml(BufferedReader reader) { NodeList sourceList = ((Element) rootList.item(0)).getElementsByTagNameNS("*", "Source"); for (int i = 0; i < sourceList.getLength(); i++) { MSBibEntry entry = new MSBibEntry((Element) sourceList.item(i)); - entries.add(entry); + entriesForExport.add(entry); bibitems.add(BibTeXConverter.convert(entry)); } return bibitems; } - private void addEntriesForExport(List entriesToAdd) { - entries = new HashSet<>(); + private void setEntriesForExport(List entriesToAdd) { + entriesForExport = new HashSet<>(); for (BibEntry entry : entriesToAdd) { MSBibEntry newMods = MSBibConverter.convert(entry); - entries.add(newMods); + entriesForExport.add(newMods); } } - /** - * Gets the assembled dom for export - * - * @return XML Document - */ public Document getDomForExport() { Document document = null; try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); DocumentBuilder documentBuilder = factory.newDocumentBuilder(); document = documentBuilder.newDocument(); @@ -126,7 +121,7 @@ public Document getDomForExport() { "xmlns:" + PREFIX.substring(0, PREFIX.length() - 1), NAMESPACE); rootNode.setAttribute("SelectedStyle", ""); - for (MSBibEntry entry : entries) { + for (MSBibEntry entry : entriesForExport) { Node node = entry.getEntryDom(document); rootNode.appendChild(node); } diff --git a/src/main/java/org/jabref/logic/msbib/MSBibEntry.java b/src/main/java/org/jabref/logic/msbib/MSBibEntry.java index 28618a6f60e..2366c3a5159 100644 --- a/src/main/java/org/jabref/logic/msbib/MSBibEntry.java +++ b/src/main/java/org/jabref/logic/msbib/MSBibEntry.java @@ -28,8 +28,8 @@ */ class MSBibEntry { - // MSBib fields and values public Map fields = new HashMap<>(); + public List authors; public List bookAuthors; public List editors; @@ -74,7 +74,6 @@ class MSBibEntry { * Matches both single locations (only city) like Berlin and full locations like Stroudsburg, PA, USA
* tested using http://www.regexpal.com/ */ - private final Pattern ADDRESS_PATTERN = Pattern.compile("\\b(\\w+)\\s?[,]?\\s?(\\w*)\\s?[,]?\\s?(\\w*)\\b"); public MSBibEntry() { @@ -82,7 +81,7 @@ public MSBibEntry() { } /** - * Create a new {@link MsBibEntry} to import from an xml element + * Create a new {@link MSBibEntry} to import from an XML element */ public MSBibEntry(Element entry) { populateFromXml(entry); @@ -311,14 +310,14 @@ private void addAuthor(Document document, Element allAuthors, String entryName, } Element authorTop = document.createElementNS(MSBibDatabase.NAMESPACE, MSBibDatabase.PREFIX + entryName); - Optional personName = authorsLst.stream().filter(MsBibAuthor::isCorporate) + Optional personName = authorsLst.stream() + .filter(MsBibAuthor::isCorporate) .findFirst(); if (personName.isPresent()) { MsBibAuthor person = personName.get(); - Element corporate = document.createElementNS(MSBibDatabase.NAMESPACE, MSBibDatabase.PREFIX + "Corporate"); - corporate.setTextContent(person.getFirstLast()); + corporate.setTextContent(person.getLastName()); authorTop.appendChild(corporate); } else { Element nameList = document.createElementNS(MSBibDatabase.NAMESPACE, MSBibDatabase.PREFIX + "NameList"); diff --git a/src/main/java/org/jabref/logic/msbib/MSBibMapping.java b/src/main/java/org/jabref/logic/msbib/MSBibMapping.java index d5abe4dbc15..ce27cdcf381 100644 --- a/src/main/java/org/jabref/logic/msbib/MSBibMapping.java +++ b/src/main/java/org/jabref/logic/msbib/MSBibMapping.java @@ -22,13 +22,13 @@ public class MSBibMapping { private static final String BIBTEX_PREFIX = "BIBTEX_"; private static final String MSBIB_PREFIX = "msbib-"; - - private static final BiMap BIBLATEX_TO_MS_BIB = HashBiMap.create(); - - // see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a private static final BiMap LANG_TO_LCID = HashBiMap.create(); + private static final BiMap BIBLATEX_TO_MS_BIB = HashBiMap.create(); + private static final Map MSBIB_ENTRYTYPE_MAPPING; + private static final Map BIB_ENTRYTYPE_MAPPING = new HashMap<>(); static { + // see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a LANG_TO_LCID.put("basque", 1609); LANG_TO_LCID.put("bulgarian", 1026); LANG_TO_LCID.put("catalan", 1027); @@ -120,47 +120,49 @@ public class MSBibMapping { BIBLATEX_TO_MS_BIB.put(new UnknownField(MSBIB_PREFIX + "productioncompany"), "ProductionCompany"); } + static { + MSBIB_ENTRYTYPE_MAPPING = Map.of( + "Book", StandardEntryType.Book, + "BookSection", StandardEntryType.Book, + "JournalArticle", StandardEntryType.Article, + "ArticleInAPeriodical", IEEETranEntryType.Periodical, + "ConferenceProceedings", StandardEntryType.InProceedings, + "Report", StandardEntryType.TechReport, + "Patent", IEEETranEntryType.Patent, + "InternetSite", StandardEntryType.Online); + } + + static { + // We need to add the entries "manually", because Map.of does not allow that many entries + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Book, MSBibEntryType.Book); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.InBook, MSBibEntryType.BookSection); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Booklet, MSBibEntryType.BookSection); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.InCollection, MSBibEntryType.BookSection); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Article, MSBibEntryType.JournalArticle); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.InProceedings, MSBibEntryType.ConferenceProceedings); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Conference, MSBibEntryType.ConferenceProceedings); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Proceedings, MSBibEntryType.ConferenceProceedings); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Collection, MSBibEntryType.ConferenceProceedings); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.TechReport, MSBibEntryType.Report); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Manual, MSBibEntryType.Report); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.MastersThesis, MSBibEntryType.Report); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.PhdThesis, MSBibEntryType.Report); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Unpublished, MSBibEntryType.Report); + BIB_ENTRYTYPE_MAPPING.put(IEEETranEntryType.Patent, MSBibEntryType.Patent); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Misc, MSBibEntryType.Misc); + BIB_ENTRYTYPE_MAPPING.put(IEEETranEntryType.Electronic, MSBibEntryType.ElectronicSource); + BIB_ENTRYTYPE_MAPPING.put(StandardEntryType.Online, MSBibEntryType.InternetSite); + } + private MSBibMapping() { } public static EntryType getBiblatexEntryType(String msbibType) { - Map entryTypeMapping = new HashMap<>(); - - entryTypeMapping.put("Book", StandardEntryType.Book); - entryTypeMapping.put("BookSection", StandardEntryType.Book); - entryTypeMapping.put("JournalArticle", StandardEntryType.Article); - entryTypeMapping.put("ArticleInAPeriodical", IEEETranEntryType.Periodical); - entryTypeMapping.put("ConferenceProceedings", StandardEntryType.InProceedings); - entryTypeMapping.put("Report", StandardEntryType.TechReport); - entryTypeMapping.put("Patent", IEEETranEntryType.Patent); - entryTypeMapping.put("InternetSite", StandardEntryType.Online); - - return entryTypeMapping.getOrDefault(msbibType, StandardEntryType.Misc); + return MSBIB_ENTRYTYPE_MAPPING.getOrDefault(msbibType, StandardEntryType.Misc); } public static MSBibEntryType getMSBibEntryType(EntryType bibtexType) { - Map entryTypeMapping = new HashMap<>(); - - entryTypeMapping.put(StandardEntryType.Book, MSBibEntryType.Book); - entryTypeMapping.put(StandardEntryType.InBook, MSBibEntryType.BookSection); - entryTypeMapping.put(StandardEntryType.Booklet, MSBibEntryType.BookSection); - entryTypeMapping.put(StandardEntryType.InCollection, MSBibEntryType.BookSection); - entryTypeMapping.put(StandardEntryType.Article, MSBibEntryType.JournalArticle); - entryTypeMapping.put(StandardEntryType.InProceedings, MSBibEntryType.ConferenceProceedings); - entryTypeMapping.put(StandardEntryType.Conference, MSBibEntryType.ConferenceProceedings); - entryTypeMapping.put(StandardEntryType.Proceedings, MSBibEntryType.ConferenceProceedings); - entryTypeMapping.put(StandardEntryType.Collection, MSBibEntryType.ConferenceProceedings); - entryTypeMapping.put(StandardEntryType.TechReport, MSBibEntryType.Report); - entryTypeMapping.put(StandardEntryType.Manual, MSBibEntryType.Report); - entryTypeMapping.put(StandardEntryType.MastersThesis, MSBibEntryType.Report); - entryTypeMapping.put(StandardEntryType.PhdThesis, MSBibEntryType.Report); - entryTypeMapping.put(StandardEntryType.Unpublished, MSBibEntryType.Report); - entryTypeMapping.put(IEEETranEntryType.Patent, MSBibEntryType.Patent); - entryTypeMapping.put(StandardEntryType.Misc, MSBibEntryType.Misc); - entryTypeMapping.put(IEEETranEntryType.Electronic, MSBibEntryType.ElectronicSource); - entryTypeMapping.put(StandardEntryType.Online, MSBibEntryType.InternetSite); - - return entryTypeMapping.getOrDefault(bibtexType, MSBibEntryType.Misc); + return BIB_ENTRYTYPE_MAPPING.getOrDefault(bibtexType, MSBibEntryType.Misc); } /** diff --git a/src/main/java/org/jabref/logic/msbib/MsBibAuthor.java b/src/main/java/org/jabref/logic/msbib/MsBibAuthor.java index 1c1bd488390..bcceef9657f 100644 --- a/src/main/java/org/jabref/logic/msbib/MsBibAuthor.java +++ b/src/main/java/org/jabref/logic/msbib/MsBibAuthor.java @@ -1,9 +1,12 @@ package org.jabref.logic.msbib; +import org.jabref.logic.formatter.bibtexfields.RemoveBracesFormatter; import org.jabref.model.entry.Author; public class MsBibAuthor { + private static final RemoveBracesFormatter REMOVE_BRACES_FORMATTER = new RemoveBracesFormatter(); + private String firstName; private String middleName; private final Author author; @@ -13,7 +16,7 @@ public MsBibAuthor(Author author) { this.author = author; StringBuilder sb = new StringBuilder(); - author.getFirst().ifPresent(firstNames -> { + author.getGivenName().ifPresent(firstNames -> { String[] names = firstNames.split(" "); for (int i = 1; i < names.length; i++) { @@ -34,7 +37,7 @@ public String getFirstName() { if (!"".equals(firstName)) { return firstName; } - return author.getFirst().orElse(null); + return author.getGivenName().orElse(null); } public String getMiddleName() { @@ -45,15 +48,15 @@ public String getMiddleName() { } public String getLastName() { - return author.getLastOnly(); + return REMOVE_BRACES_FORMATTER.format(author.getNamePrefixAndFamilyName()); } public String getFirstLast() { - return author.getFirstLast(false); + return author.getGivenFamily(false); } public String getLastFirst() { - return author.getLastFirst(false); + return author.getFamilyGiven(false); } public boolean isCorporate() { diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java index f788c809474..c38e3d37ed0 100644 --- a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java +++ b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java @@ -3,9 +3,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; +import org.jabref.logic.formatter.bibtexfields.RemoveBracesFormatter; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; @@ -20,8 +20,12 @@ import org.jabref.model.openoffice.style.PageInfo; import org.jabref.model.strings.StringUtil; +import org.jspecify.annotations.NonNull; + class OOBibStyleGetCitationMarker { + private static final RemoveBracesFormatter REMOVE_BRACES_FORMATTER = new RemoveBracesFormatter(); + private OOBibStyleGetCitationMarker() { } @@ -42,13 +46,13 @@ private static String getAuthorLastName(AuthorList authorList, int number) { if (authorList.getNumberOfAuthors() > number) { Author author = authorList.getAuthor(number); // "von " if von exists - Optional von = author.getVon(); + Optional von = author.getNamePrefix(); if (von.isPresent() && !von.get().isEmpty()) { stringBuilder.append(von.get()); stringBuilder.append(' '); } // last name if it exists - stringBuilder.append(author.getLast().orElse("")); + stringBuilder.append(author.getFamilyName().map(last -> REMOVE_BRACES_FORMATTER.format(last)).orElse("")); } return stringBuilder.toString(); @@ -87,12 +91,9 @@ private static String markupAuthorName(OOBibStyle style, String name) { * - andString is only emitted if nAuthors is at least 2. */ private static String formatAuthorList(OOBibStyle style, - AuthorList authorList, + @NonNull AuthorList authorList, int maxAuthors, String andString) { - - Objects.requireNonNull(authorList); - // Apparently maxAuthorsBeforeEtAl is always 1 for in-text citations. // In reference lists can be for example 7, // (https://www.chicagomanualofstyle.org/turabian/turabian-author-date-citation-quick-guide.html) @@ -224,16 +225,13 @@ private static class FieldAndContent { * field (or alias) from {@code fields} found in {@code entry}. * Return {@code Optional.empty()} if found nothing. */ - private static Optional getRawCitationMarkerField(BibEntry entry, - BibDatabase database, - OrFields fields) { - Objects.requireNonNull(entry, "Entry cannot be null"); - Objects.requireNonNull(database, "database cannot be null"); - + private static Optional getRawCitationMarkerField(@NonNull BibEntry entry, + @NonNull BibDatabase database, + @NonNull OrFields fields) { for (Field field : fields.getFields() /* FieldFactory.parseOrFields(fields)*/) { + // NOT LaTeX free, because there is some latextohtml in org.jabref.logic.openoffice.style.OOPreFormatter Optional optionalContent = entry.getResolvedFieldOrAlias(field, database); - final boolean foundSomething = optionalContent.isPresent() - && !optionalContent.get().trim().isEmpty(); + final boolean foundSomething = !StringUtil.isBlank(optionalContent); if (foundSomething) { return Optional.of(new FieldAndContent(field, optionalContent.get())); } @@ -264,10 +262,8 @@ private static Optional getRawCitationMarkerField(BibEntry entr * */ private static String getCitationMarkerField(OOBibStyle style, - CitationLookupResult db, + @NonNull CitationLookupResult db, OrFields fields) { - Objects.requireNonNull(db); - Optional optionalFieldAndContent = getRawCitationMarkerField(db.entry, db.database, fields); diff --git a/src/main/java/org/jabref/logic/xmp/DublinCoreExtractor.java b/src/main/java/org/jabref/logic/xmp/DublinCoreExtractor.java index 13ea76677b1..8e0cfcf20d9 100644 --- a/src/main/java/org/jabref/logic/xmp/DublinCoreExtractor.java +++ b/src/main/java/org/jabref/logic/xmp/DublinCoreExtractor.java @@ -299,7 +299,7 @@ public Optional extractBibtexEntry() { private void fillContributor(String authors) { AuthorList list = AuthorList.parse(authors); for (Author author : list.getAuthors()) { - dcSchema.addContributor(author.getFirstLast(false)); + dcSchema.addContributor(author.getGivenFamily(false)); } } @@ -309,7 +309,7 @@ private void fillContributor(String authors) { private void fillCreator(String creators) { AuthorList list = AuthorList.parse(creators); for (Author author : list.getAuthors()) { - dcSchema.addCreator(author.getFirstLast(false)); + dcSchema.addCreator(author.getGivenFamily(false)); } } diff --git a/src/main/java/org/jabref/model/entry/Author.java b/src/main/java/org/jabref/model/entry/Author.java index fd45ddb1e05..e4511e7ef07 100644 --- a/src/main/java/org/jabref/model/entry/Author.java +++ b/src/main/java/org/jabref/model/entry/Author.java @@ -20,28 +20,38 @@ public class Author { */ public static final Author OTHERS = new Author("", "", null, "others", null); - private final String firstPart; - private final String firstAbbr; - private final String vonPart; - private final String lastPart; - private final String jrPart; + private final String givenName; + private final String givenNameAbbreviated; + private final String namePrefix; + private final String familyName; + private final String nameSuffix; private Author latexFreeAuthor; /** * Creates the Author object. If any part of the name is absent, null must be passed; otherwise other methods may return erroneous results. + *

+ * In case only the last part is passed, enclosing braces are * - * @param first the first name of the author (may consist of several tokens, like "Charles Louis Xavier Joseph" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") - * @param firstabbr the abbreviated first name of the author (may consist of several tokens, like "C. L. X. J." in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin"). It is a responsibility of the caller to create a reasonable abbreviation of the first name. - * @param von the von part of the author's name (may consist of several tokens, like "de la" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") - * @param last the last name of the author (may consist of several tokens, like "Vall{\'e}e Poussin" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") - * @param jr the junior part of the author's name (may consist of several tokens, like "Jr. III" in "Smith, Jr. III, John") + * @param givenName the first name of the author (may consist of several tokens, like "Charles Louis Xavier Joseph" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") + * @param givenNameAbbreviated the abbreviated first name of the author (may consist of several tokens, like "C. L. X. J." in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin"). It is a responsibility of the caller to create a reasonable abbreviation of the first name. + * @param namePrefix the von part of the author's name (may consist of several tokens, like "de la" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") + * @param familyName the last name of the author (may consist of several tokens, like "Vall{\'e}e Poussin" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") + * @param nameSuffix the junior part of the author's name (may consist of several tokens, like "Jr. III" in "Smith, Jr. III, John") */ - public Author(String first, String firstabbr, String von, String last, String jr) { - firstPart = addDotIfAbbreviation(removeStartAndEndBraces(first)); - firstAbbr = removeStartAndEndBraces(firstabbr); - vonPart = removeStartAndEndBraces(von); - lastPart = removeStartAndEndBraces(last); - jrPart = removeStartAndEndBraces(jr); + public Author(String givenName, String givenNameAbbreviated, String namePrefix, String familyName, String nameSuffix) { + boolean keepBracesAtLastPart = StringUtil.isBlank(givenName) && StringUtil.isBlank(givenNameAbbreviated) && StringUtil.isBlank(namePrefix) && !StringUtil.isBlank(familyName) && StringUtil.isBlank(nameSuffix); + + this.givenName = addDotIfAbbreviation(removeStartAndEndBraces(givenName)); + this.givenNameAbbreviated = removeStartAndEndBraces(givenNameAbbreviated); + this.namePrefix = removeStartAndEndBraces(namePrefix); + if (keepBracesAtLastPart) { + // We do not remove braces here to keep institutions protected + // https://github.com/JabRef/jabref/issues/10031 + this.familyName = familyName; + } else { + this.familyName = removeStartAndEndBraces(familyName); + } + this.nameSuffix = removeStartAndEndBraces(nameSuffix); } public static String addDotIfAbbreviation(String name) { @@ -130,7 +140,7 @@ public static String addDotIfAbbreviation(String name) { @Override public int hashCode() { - return Objects.hash(firstAbbr, firstPart, jrPart, lastPart, vonPart); + return Objects.hash(givenNameAbbreviated, givenName, nameSuffix, familyName, namePrefix); } /** @@ -145,11 +155,11 @@ public boolean equals(Object other) { } if (other instanceof Author that) { - return Objects.equals(firstPart, that.firstPart) - && Objects.equals(firstAbbr, that.firstAbbr) - && Objects.equals(vonPart, that.vonPart) - && Objects.equals(lastPart, that.lastPart) - && Objects.equals(jrPart, that.jrPart); + return Objects.equals(givenName, that.givenName) + && Objects.equals(givenNameAbbreviated, that.givenNameAbbreviated) + && Objects.equals(namePrefix, that.namePrefix) + && Objects.equals(familyName, that.familyName) + && Objects.equals(nameSuffix, that.nameSuffix); } return false; @@ -183,7 +193,7 @@ private boolean properBrackets(String s) { } /** - * Removes start and end brace at a string + * Removes start and end brace both at the complete string and at beginning/end of a word *

* E.g., *

    @@ -248,8 +258,8 @@ private String removeStartAndEndBraces(String name) { * * @return first name of the author (may consist of several tokens) */ - public Optional getFirst() { - return Optional.ofNullable(firstPart); + public Optional getGivenName() { + return Optional.ofNullable(givenName); } /** @@ -257,17 +267,17 @@ public Optional getFirst() { * * @return abbreviated first name of the author (may consist of several tokens) */ - public Optional getFirstAbbr() { - return Optional.ofNullable(firstAbbr); + public Optional getGivenNameAbbreviated() { + return Optional.ofNullable(givenNameAbbreviated); } /** - * Returns the von part of the author's name stored in this object ("von"). + * Returns the von part of the author's name stored in this object ("von", "name prefix"). * * @return von part of the author's name (may consist of several tokens) */ - public Optional getVon() { - return Optional.ofNullable(vonPart); + public Optional getNamePrefix() { + return Optional.ofNullable(namePrefix); } /** @@ -275,17 +285,17 @@ public Optional getVon() { * * @return last name of the author (may consist of several tokens) */ - public Optional getLast() { - return Optional.ofNullable(lastPart); + public Optional getFamilyName() { + return Optional.ofNullable(familyName); } /** - * Returns the junior part of the author's name stored in this object ("Jr"). + * Returns the name suffix ("junior") part of the author's name stored in this object ("Jr"). * * @return junior part of the author's name (may consist of several tokens) or null if the author does not have a Jr. Part */ - public Optional getJr() { - return Optional.ofNullable(jrPart); + public Optional getNameSuffix() { + return Optional.ofNullable(nameSuffix); } /** @@ -293,11 +303,11 @@ public Optional getJr() { * * @return 'von Last' */ - public String getLastOnly() { - if (vonPart == null) { - return getLast().orElse(""); + public String getNamePrefixAndFamilyName() { + if (namePrefix == null) { + return getFamilyName().orElse(""); } else { - return lastPart == null ? vonPart : vonPart + ' ' + lastPart; + return familyName == null ? namePrefix : namePrefix + ' ' + familyName; } } @@ -307,13 +317,13 @@ public String getLastOnly() { * @param abbr true - abbreviate first name, false - do not abbreviate * @return 'von Last, Jr., First' (if abbr==false) or 'von Last, Jr., F.' (if abbr==true) */ - public String getLastFirst(boolean abbr) { - StringBuilder res = new StringBuilder(getLastOnly()); - getJr().ifPresent(jr -> res.append(", ").append(jr)); + public String getFamilyGiven(boolean abbr) { + StringBuilder res = new StringBuilder(getNamePrefixAndFamilyName()); + getNameSuffix().ifPresent(jr -> res.append(", ").append(jr)); if (abbr) { - getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA)); + getGivenNameAbbreviated().ifPresent(firstA -> res.append(", ").append(firstA)); } else { - getFirst().ifPresent(first -> res.append(", ").append(first)); + getGivenName().ifPresent(first -> res.append(", ").append(first)); } return res.toString(); } @@ -324,26 +334,26 @@ public String getLastFirst(boolean abbr) { * @param abbr true - abbreviate first name, false - do not abbreviate * @return 'First von Last, Jr.' (if abbr==false) or 'F. von Last, Jr.' (if abbr==true) */ - public String getFirstLast(boolean abbr) { + public String getGivenFamily(boolean abbr) { StringBuilder res = new StringBuilder(); if (abbr) { - getFirstAbbr().map(firstA -> firstA + ' ').ifPresent(res::append); + getGivenNameAbbreviated().map(firstA -> firstA + ' ').ifPresent(res::append); } else { - getFirst().map(first -> first + ' ').ifPresent(res::append); + getGivenName().map(first -> first + ' ').ifPresent(res::append); } - res.append(getLastOnly()); - getJr().ifPresent(jr -> res.append(", ").append(jr)); + res.append(getNamePrefixAndFamilyName()); + getNameSuffix().ifPresent(jr -> res.append(", ").append(jr)); return res.toString(); } @Override public String toString() { final StringBuilder sb = new StringBuilder("Author{"); - sb.append("firstPart='").append(firstPart).append('\''); - sb.append(", firstAbbr='").append(firstAbbr).append('\''); - sb.append(", vonPart='").append(vonPart).append('\''); - sb.append(", lastPart='").append(lastPart).append('\''); - sb.append(", jrPart='").append(jrPart).append('\''); + sb.append("givenName='").append(givenName).append('\''); + sb.append(", givenNameAbbreviated='").append(givenNameAbbreviated).append('\''); + sb.append(", namePrefix='").append(namePrefix).append('\''); + sb.append(", familyName='").append(familyName).append('\''); + sb.append(", nameSuffix='").append(nameSuffix).append('\''); sb.append('}'); return sb.toString(); } @@ -355,9 +365,9 @@ public String toString() { */ public String getNameForAlphabetization() { StringBuilder res = new StringBuilder(); - getLast().ifPresent(res::append); - getJr().ifPresent(jr -> res.append(", ").append(jr)); - getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA)); + getFamilyName().ifPresent(res::append); + getNameSuffix().ifPresent(jr -> res.append(", ").append(jr)); + getGivenNameAbbreviated().ifPresent(firstA -> res.append(", ").append(firstA)); while ((res.length() > 0) && (res.charAt(0) == '{')) { res.deleteCharAt(0); } @@ -369,12 +379,12 @@ public String getNameForAlphabetization() { */ public Author latexFree() { if (latexFreeAuthor == null) { - String first = getFirst().map(LatexToUnicodeAdapter::format).orElse(null); - String firstabbr = getFirstAbbr().map(LatexToUnicodeAdapter::format).orElse(null); - String von = getVon().map(LatexToUnicodeAdapter::format).orElse(null); - String last = getLast().map(LatexToUnicodeAdapter::format).orElse(null); - String jr = getJr().map(LatexToUnicodeAdapter::format).orElse(null); - latexFreeAuthor = new Author(first, firstabbr, von, last, jr); + String first = getGivenName().map(LatexToUnicodeAdapter::format).orElse(null); + String givenNameAbbreviated = getGivenNameAbbreviated().map(LatexToUnicodeAdapter::format).orElse(null); + String von = getNamePrefix().map(LatexToUnicodeAdapter::format).orElse(null); + String last = getFamilyName().map(LatexToUnicodeAdapter::format).orElse(null); + String jr = getNameSuffix().map(LatexToUnicodeAdapter::format).orElse(null); + latexFreeAuthor = new Author(first, givenNameAbbreviated, von, last, jr); latexFreeAuthor.latexFreeAuthor = latexFreeAuthor; } return latexFreeAuthor; diff --git a/src/main/java/org/jabref/model/entry/AuthorList.java b/src/main/java/org/jabref/model/entry/AuthorList.java index ab2888bfd09..17b68e02fbe 100644 --- a/src/main/java/org/jabref/model/entry/AuthorList.java +++ b/src/main/java/org/jabref/model/entry/AuthorList.java @@ -12,6 +12,8 @@ import org.jabref.architecture.AllowedToUseLogic; import org.jabref.logic.importer.AuthorListParser; +import org.jspecify.annotations.NonNull; + /** * This is an immutable class representing information of either author or editor field in bibtex record. *

    @@ -169,16 +171,11 @@ private static String andCoordinatedConjunction(List authors, boolean ox * @param authors The string of authors or editors in bibtex format to parse. * @return An AuthorList object representing the given authors. */ - public static AuthorList parse(final String authors) { - Objects.requireNonNull(authors); - - AuthorList authorList = AUTHOR_CACHE.get(authors); - if (authorList == null) { + public static AuthorList parse(@NonNull final String authors) { + return AUTHOR_CACHE.computeIfAbsent(authors, string -> { AuthorListParser parser = new AuthorListParser(); - authorList = parser.parse(authors); - AUTHOR_CACHE.put(authors, authorList); - } - return authorList; + return parser.parse(string); + }); } /** @@ -319,9 +316,9 @@ public String getAsNatbib() { var authors = getAuthors(); return switch (authors.size()) { case 0 -> ""; - case 1 -> authors.getFirst().getLastOnly(); - case 2 -> authors.getFirst().getLastOnly() + " and " + authors.get(1).getLastOnly(); - default -> authors.getFirst().getLastOnly() + " et al."; + case 1 -> authors.getFirst().getNamePrefixAndFamilyName(); + case 2 -> authors.getFirst().getNamePrefixAndFamilyName() + " and " + authors.get(1).getNamePrefixAndFamilyName(); + default -> authors.getFirst().getNamePrefixAndFamilyName() + " et al."; }; } @@ -341,7 +338,7 @@ public String getAsNatbib() { * Oxford comma. */ public String getAsLastNames(boolean oxfordComma) { - return andCoordinatedConjunction(getAuthors(), Author::getLastOnly, oxfordComma); + return andCoordinatedConjunction(getAuthors(), Author::getNamePrefixAndFamilyName, oxfordComma); } /** @@ -363,7 +360,7 @@ public String getAsLastNames(boolean oxfordComma) { * Oxford comma. */ public String getAsLastFirstNames(boolean abbreviate, boolean oxfordComma) { - return andCoordinatedConjunction(getAuthors(), auth -> auth.getLastFirst(abbreviate), oxfordComma); + return andCoordinatedConjunction(getAuthors(), auth -> auth.getFamilyGiven(abbreviate), oxfordComma); } @Override @@ -386,25 +383,25 @@ public String toString() { */ public String getAsLastFirstNamesWithAnd(boolean abbreviate) { return getAuthors().stream() - .map(author -> author.getLastFirst(abbreviate)) + .map(author -> author.getFamilyGiven(abbreviate)) .collect(Collectors.joining(" and ")); } /** - * Returns a list of authors separated with "and". The first author is formatted with {@link Author#getLastFirst(boolean)} and each subsequent author is formatted with {@link Author#getFirstLast(boolean)}. + * Returns a list of authors separated with "and". The first author is formatted with {@link Author#getFamilyGiven(boolean)} and each subsequent author is formatted with {@link Author#getGivenFamily(boolean)}. * * @param abbreviate first names. */ public String getAsLastFirstFirstLastNamesWithAnd(boolean abbreviate) { return switch (authors.size()) { case 0 -> ""; - case 1 -> authors.getFirst().getLastFirst(abbreviate); + case 1 -> authors.getFirst().getFamilyGiven(abbreviate); default -> authors.stream() .skip(1) - .map(author -> author.getFirstLast(abbreviate)) + .map(author -> author.getGivenFamily(abbreviate)) .collect(Collectors.joining( " and ", - authors.getFirst().getLastFirst(abbreviate) + " and ", + authors.getFirst().getFamilyGiven(abbreviate) + " and ", "")); }; } @@ -428,7 +425,7 @@ public String getAsLastFirstFirstLastNamesWithAnd(boolean abbreviate) { * Oxford comma. */ public String getAsFirstLastNames(boolean abbreviate, boolean oxfordComma) { - return andCoordinatedConjunction(getAuthors(), author -> author.getFirstLast(abbreviate), oxfordComma); + return andCoordinatedConjunction(getAuthors(), author -> author.getGivenFamily(abbreviate), oxfordComma); } /** @@ -469,7 +466,7 @@ public int hashCode() { */ public String getAsFirstLastNamesWithAnd() { return getAuthors().stream() - .map(author -> author.getFirstLast(false)) + .map(author -> author.getGivenFamily(false)) .collect(Collectors.joining(" and ")); } diff --git a/src/main/java/org/jabref/model/groups/LastNameGroup.java b/src/main/java/org/jabref/model/groups/LastNameGroup.java index 8fa6fff938c..7ee022af1b6 100644 --- a/src/main/java/org/jabref/model/groups/LastNameGroup.java +++ b/src/main/java/org/jabref/model/groups/LastNameGroup.java @@ -25,7 +25,7 @@ static List getAsLastNamesLatexFree(Field field, BibEntry bibEntry) { .map(AuthorList::latexFree) .map(AuthorList::getAuthors) .flatMap(Collection::stream) - .map(Author::getLast) + .map(Author::getFamilyName) .flatMap(Optional::stream) .collect(Collectors.toList()); } diff --git a/src/test/java/org/jabref/logic/exporter/MSBibExportFormatFilesTest.java b/src/test/java/org/jabref/logic/exporter/MSBibExportFormatFilesTest.java index e3cc7deb7d0..6a03d17cd3c 100644 --- a/src/test/java/org/jabref/logic/exporter/MSBibExportFormatFilesTest.java +++ b/src/test/java/org/jabref/logic/exporter/MSBibExportFormatFilesTest.java @@ -7,7 +7,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.jabref.logic.importer.ImportFormatPreferences; @@ -44,8 +43,8 @@ static Stream fileNames() throws IOException, URISyntaxException { return stream.map(n -> n.getFileName().toString()) .filter(n -> n.endsWith(".bib")) .filter(n -> n.startsWith("MsBib")) - .collect(Collectors.toList()) - .stream(); + // mapping required, because we get "source already consumed or closed" otherwise + .toList().stream(); } } @@ -75,7 +74,7 @@ void performExport(String filename) throws IOException, SaveException { String expected = String.join("\n", Files.readAllLines(expectedFile)); String actual = String.join("\n", Files.readAllLines(exportedFile)); - // The order of elements changes from Windows to Travis environment somehow + // The order of the XML elements changes from Windows to Travis environment somehow // The order does not really matter, so we ignore it. // Source: https://stackoverflow.com/a/16540679/873282 assertThat(expected, isSimilarTo(actual) diff --git a/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeNamesFormatterTest.java b/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeNamesFormatterTest.java index 715e532b781..2c8f81058a2 100644 --- a/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeNamesFormatterTest.java +++ b/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeNamesFormatterTest.java @@ -19,6 +19,9 @@ public void setUp() { @Test public void normalizeAuthorList() { + assertEquals("{Society of Automotive Engineers}", formatter.format("{Society of Automotive Engineers}")); + assertEquals("{Company Name, LLC}", formatter.format("{Company Name, LLC}")); + assertEquals("Bilbo, Staci D.", formatter.format("Staci D Bilbo")); assertEquals("Bilbo, Staci D.", formatter.format("Staci D. Bilbo")); diff --git a/src/test/java/org/jabref/logic/importer/AuthorListParserTest.java b/src/test/java/org/jabref/logic/importer/AuthorListParserTest.java index 42a3c18b378..2943fc5fec6 100644 --- a/src/test/java/org/jabref/logic/importer/AuthorListParserTest.java +++ b/src/test/java/org/jabref/logic/importer/AuthorListParserTest.java @@ -20,7 +20,7 @@ private static Stream parseSingleAuthorCorrectly() { Arguments.of("王, 军", new Author("军", "军.", null, "王", null)), Arguments.of("Doe, John", new Author("John", "J.", null, "Doe", null)), Arguments.of("von Berlichingen zu Hornberg, Johann Gottfried", new Author("Johann Gottfried", "J. G.", "von", "Berlichingen zu Hornberg", null)), - Arguments.of("{Robert and Sons, Inc.}", new Author(null, null, null, "Robert and Sons, Inc.", null)), + Arguments.of("{Robert and Sons, Inc.}", new Author(null, null, null, "{Robert and Sons, Inc.}", null)), Arguments.of("al-Ṣāliḥ, Abdallāh", new Author("Abdallāh", "A.", null, "al-Ṣāliḥ", null)), Arguments.of("de la Vallée Poussin, Jean Charles Gabriel", new Author("Jean Charles Gabriel", "J. C. G.", "de la", "Vallée Poussin", null)), Arguments.of("de la Vallée Poussin, J. C. G.", new Author("J. C. G.", "J. C. G.", "de la", "Vallée Poussin", null)), @@ -28,7 +28,12 @@ private static Stream parseSingleAuthorCorrectly() { Arguments.of("Uhlenhaut, N Henriette", new Author("N Henriette", "N. H.", null, "Uhlenhaut", null)), Arguments.of("Nu{\\~{n}}ez, Jose", new Author("Jose", "J.", null, "Nu{\\~{n}}ez", null)), // parseAuthorWithFirstNameAbbreviationContainingUmlaut - Arguments.of("{\\OE}rjan Umlauts", new Author("{\\OE}rjan", "{\\OE}.", null, "Umlauts", null)) + Arguments.of("{\\OE}rjan Umlauts", new Author("{\\OE}rjan", "{\\OE}.", null, "Umlauts", null)), + Arguments.of("{Company Name, LLC}", new Author("", "", null, "{Company Name, LLC}", null)), + Arguments.of("{Society of Automotive Engineers}", new Author("", "", null, "{Society of Automotive Engineers}", null)), + + // Demonstrate the "von" part parsing of a non-braced name + Arguments.of("Society of Automotive Engineers", new Author("Society", "S.", "of", "Automotive Engineers", null)) ); } diff --git a/src/test/java/org/jabref/logic/msbib/MSBibConverterTest.java b/src/test/java/org/jabref/logic/msbib/MSBibConverterTest.java index db4d1b305a6..c254cbcc478 100644 --- a/src/test/java/org/jabref/logic/msbib/MSBibConverterTest.java +++ b/src/test/java/org/jabref/logic/msbib/MSBibConverterTest.java @@ -1,7 +1,5 @@ package org.jabref.logic.msbib; -import java.util.Optional; - import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; @@ -30,13 +28,9 @@ class MSBibConverterTest { .withField(StandardField.TITLE, "Overcoming Open Source Project Entry Barriers with a Portal for Newcomers") .withField(StandardField.FILE, ":https\\://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=7886910:PDF"); - private BibEntry entry; - @Test void convert() { - entry = BIB_ENTRY_TEST; - MSBibConverter.convert(entry); - - assertEquals(Optional.of("english"), entry.getField(StandardField.LANGUAGE)); + MSBibEntry result = MSBibConverter.convert(BIB_ENTRY_TEST); + assertEquals("1033", result.fields.get("LCID")); } } diff --git a/src/test/java/org/jabref/logic/msbib/MsBibAuthorTest.java b/src/test/java/org/jabref/logic/msbib/MsBibAuthorTest.java index 703a74d0201..cbf128bf6da 100644 --- a/src/test/java/org/jabref/logic/msbib/MsBibAuthorTest.java +++ b/src/test/java/org/jabref/logic/msbib/MsBibAuthorTest.java @@ -10,7 +10,7 @@ public class MsBibAuthorTest { @Test - public void getFirstName() { + public void getGivenNameName() { Author author = new Author("Gustav Peter Johann", null, null, "Bach", null); MsBibAuthor msBibAuthor = new MsBibAuthor(author); assertEquals("Gustav", msBibAuthor.getFirstName()); @@ -38,14 +38,14 @@ public void getNoFirstName() { } @Test - public void getLastName() { + public void getFamilyNameName() { Author author = new Author("Gustav Peter Johann", null, null, "Bach", null); MsBibAuthor msBibAuthor = new MsBibAuthor(author); assertEquals("Bach", msBibAuthor.getLastName()); } @Test - public void getVonAndLastName() { + public void getNamePrefixAndLastName() { Author author = new Author("John", null, "von", "Neumann", null); MsBibAuthor msBibAuthor = new MsBibAuthor(author); assertEquals("von Neumann", msBibAuthor.getLastName()); diff --git a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java index 0f8ca79b160..109c857f7f1 100644 --- a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java +++ b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java @@ -372,19 +372,15 @@ void institutionAuthorMarker() throws IOException { abbreviationRepository); Map entryDBMap = new HashMap<>(); - List entries = new ArrayList<>(); - BibDatabase database = new BibDatabase(); - BibEntry entry = new BibEntry(); - entry.setCitationKey("JabRef2016"); - entry.setType(StandardEntryType.Article); - entry.setField(StandardField.AUTHOR, "{JabRef Development Team}"); - entry.setField(StandardField.TITLE, "JabRef Manual"); - entry.setField(StandardField.YEAR, "2016"); - database.insertEntry(entry); - entries.add(entry); + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("JabRef2016") + .withField(StandardField.AUTHOR, "{JabRef Development Team}") + .withField(StandardField.TITLE, "JabRef Manual") + .withField(StandardField.YEAR, "2016"); + List entries = List.of(entry); + BibDatabase database = new BibDatabase(entries); entryDBMap.put(entry, database); - assertEquals("[JabRef Development Team, 2016]", getCitationMarker2(style, entries, entryDBMap, true, null, null, null)); diff --git a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java index 0a6fdeea6e6..804056b031b 100644 --- a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java +++ b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java @@ -153,8 +153,8 @@ static CitationMarkerEntry makeCitationMarkerEntry(BibEntry entry, return result; } - /* - * Similar to old API. pageInfo is new, and unlimAuthors is + /** + * @implNote Similar to old API. pageInfo is new, and unlimAuthors is * replaced with isFirstAppearanceOfSource */ static String getCitationMarker2ab(OOBibStyle style, @@ -177,7 +177,7 @@ static String getCitationMarker2ab(OOBibStyle style, isFirstAppearanceOfSource = new Boolean[entries.size()]; Arrays.fill(isFirstAppearanceOfSource, false); } - List citationMarkerEntries = new ArrayList<>(); + List citationMarkerEntries = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { BibEntry entry = entries.get(i); CitationMarkerEntry e = makeCitationMarkerEntry(entry, diff --git a/src/test/java/org/jabref/logic/openoffice/style/OOPreFormatterTest.java b/src/test/java/org/jabref/logic/openoffice/style/OOPreFormatterTest.java index 3c4d828dbec..790483fd57c 100644 --- a/src/test/java/org/jabref/logic/openoffice/style/OOPreFormatterTest.java +++ b/src/test/java/org/jabref/logic/openoffice/style/OOPreFormatterTest.java @@ -14,6 +14,11 @@ public void plainFormat() { assertEquals("\\", new OOPreFormatter().format("\\\\")); } + @Test + public void removeBraces() { + assertEquals("aaa", new OOPreFormatter().format("{aaa}")); + } + @Test public void formatAccents() { assertEquals("ä", new OOPreFormatter().format("{\\\"{a}}")); diff --git a/src/test/java/org/jabref/model/entry/AuthorListTest.java b/src/test/java/org/jabref/model/entry/AuthorListTest.java index 11fd5a8e35f..6f49c711bae 100644 --- a/src/test/java/org/jabref/model/entry/AuthorListTest.java +++ b/src/test/java/org/jabref/model/entry/AuthorListTest.java @@ -734,50 +734,50 @@ public void getEmptyAuthor() { @Test public void getAuthor() { Author author = AuthorList.parse("John Smith and von Neumann, Jr, John").getAuthor(0); - assertEquals(Optional.of("John"), author.getFirst()); - assertEquals(Optional.of("J."), author.getFirstAbbr()); - assertEquals("John Smith", author.getFirstLast(false)); - assertEquals("J. Smith", author.getFirstLast(true)); - assertEquals(Optional.empty(), author.getJr()); - assertEquals(Optional.of("Smith"), author.getLast()); - assertEquals("Smith, John", author.getLastFirst(false)); - assertEquals("Smith, J.", author.getLastFirst(true)); - assertEquals("Smith", author.getLastOnly()); + assertEquals(Optional.of("John"), author.getGivenName()); + assertEquals(Optional.of("J."), author.getGivenNameAbbreviated()); + assertEquals("John Smith", author.getGivenFamily(false)); + assertEquals("J. Smith", author.getGivenFamily(true)); + assertEquals(Optional.empty(), author.getNameSuffix()); + assertEquals(Optional.of("Smith"), author.getFamilyName()); + assertEquals("Smith, John", author.getFamilyGiven(false)); + assertEquals("Smith, J.", author.getFamilyGiven(true)); + assertEquals("Smith", author.getNamePrefixAndFamilyName()); assertEquals("Smith, J.", author.getNameForAlphabetization()); - assertEquals(Optional.empty(), author.getVon()); + assertEquals(Optional.empty(), author.getNamePrefix()); author = AuthorList.parse("Peter Black Brown").getAuthor(0); - assertEquals(Optional.of("Peter Black"), author.getFirst()); - assertEquals(Optional.of("P. B."), author.getFirstAbbr()); - assertEquals("Peter Black Brown", author.getFirstLast(false)); - assertEquals("P. B. Brown", author.getFirstLast(true)); - assertEquals(Optional.empty(), author.getJr()); - assertEquals(Optional.empty(), author.getVon()); + assertEquals(Optional.of("Peter Black"), author.getGivenName()); + assertEquals(Optional.of("P. B."), author.getGivenNameAbbreviated()); + assertEquals("Peter Black Brown", author.getGivenFamily(false)); + assertEquals("P. B. Brown", author.getGivenFamily(true)); + assertEquals(Optional.empty(), author.getNameSuffix()); + assertEquals(Optional.empty(), author.getNamePrefix()); author = AuthorList.parse("John Smith and von Neumann, Jr, John").getAuthor(1); - assertEquals(Optional.of("John"), author.getFirst()); - assertEquals(Optional.of("J."), author.getFirstAbbr()); - assertEquals("John von Neumann, Jr", author.getFirstLast(false)); - assertEquals("J. von Neumann, Jr", author.getFirstLast(true)); - assertEquals(Optional.of("Jr"), author.getJr()); - assertEquals(Optional.of("Neumann"), author.getLast()); - assertEquals("von Neumann, Jr, John", author.getLastFirst(false)); - assertEquals("von Neumann, Jr, J.", author.getLastFirst(true)); - assertEquals("von Neumann", author.getLastOnly()); + assertEquals(Optional.of("John"), author.getGivenName()); + assertEquals(Optional.of("J."), author.getGivenNameAbbreviated()); + assertEquals("John von Neumann, Jr", author.getGivenFamily(false)); + assertEquals("J. von Neumann, Jr", author.getGivenFamily(true)); + assertEquals(Optional.of("Jr"), author.getNameSuffix()); + assertEquals(Optional.of("Neumann"), author.getFamilyName()); + assertEquals("von Neumann, Jr, John", author.getFamilyGiven(false)); + assertEquals("von Neumann, Jr, J.", author.getFamilyGiven(true)); + assertEquals("von Neumann", author.getNamePrefixAndFamilyName()); assertEquals("Neumann, Jr, J.", author.getNameForAlphabetization()); - assertEquals(Optional.of("von"), author.getVon()); + assertEquals(Optional.of("von"), author.getNamePrefix()); } @Test public void companyAuthor() { Author author = AuthorList.parse("{JabRef Developers}").getAuthor(0); - Author expected = new Author(null, null, null, "JabRef Developers", null); + Author expected = new Author(null, null, null, "{JabRef Developers}", null); assertEquals(expected, author); } @Test public void companyAuthorAndPerson() { - Author company = new Author(null, null, null, "JabRef Developers", null); + Author company = new Author(null, null, null, "{JabRef Developers}", null); Author person = new Author("Stefan", "S.", null, "Kolb", null); assertEquals(Arrays.asList(company, person), AuthorList.parse("{JabRef Developers} and Stefan Kolb").getAuthors()); } @@ -785,7 +785,7 @@ public void companyAuthorAndPerson() { @Test public void companyAuthorWithLowerCaseWord() { Author author = AuthorList.parse("{JabRef Developers on Fire}").getAuthor(0); - Author expected = new Author(null, null, null, "JabRef Developers on Fire", null); + Author expected = new Author(null, null, null, "{JabRef Developers on Fire}", null); assertEquals(expected, author); } @@ -985,17 +985,17 @@ public void getAuthorsForAlphabetization() { @Test public void removeStartAndEndBraces() { assertEquals("{A}bbb{c}", AuthorList.parse("{A}bbb{c}").getAsLastNames(false)); - assertEquals("Vall{\\'e}e Poussin", AuthorList.parse("{Vall{\\'e}e Poussin}").getAsLastNames(false)); + assertEquals("{Vall{\\'e}e Poussin}", AuthorList.parse("{Vall{\\'e}e Poussin}").getAsLastNames(false)); assertEquals("Poussin", AuthorList.parse("{Vall{\\'e}e} {Poussin}").getAsLastNames(false)); assertEquals("Poussin", AuthorList.parse("Vall{\\'e}e Poussin").getAsLastNames(false)); assertEquals("Lastname", AuthorList.parse("Firstname {Lastname}").getAsLastNames(false)); - assertEquals("Firstname Lastname", AuthorList.parse("{Firstname Lastname}").getAsLastNames(false)); + assertEquals("{Firstname Lastname}", AuthorList.parse("{Firstname Lastname}").getAsLastNames(false)); } @Test public void createCorrectInitials() { assertEquals(Optional.of("J. G."), - AuthorList.parse("Hornberg, Johann Gottfried").getAuthor(0).getFirstAbbr()); + AuthorList.parse("Hornberg, Johann Gottfried").getAuthor(0).getGivenNameAbbreviated()); } @Test @@ -1052,34 +1052,34 @@ public void parseNameWithBraces() throws Exception { public void parseFirstNameFromFirstAuthorMultipleAuthorsWithLatexNames() throws Exception { assertEquals("Mu{\\d{h}}ammad", AuthorList.parse("Mu{\\d{h}}ammad al-Khw{\\={a}}rizm{\\={i}} and Corrado B{\\\"o}hm") - .getAuthor(0).getFirst().orElse(null)); + .getAuthor(0).getGivenName().orElse(null)); } @Test public void parseFirstNameFromSecondAuthorMultipleAuthorsWithLatexNames() throws Exception { assertEquals("Corrado", AuthorList.parse("Mu{\\d{h}}ammad al-Khw{\\={a}}rizm{\\={i}} and Corrado B{\\\"o}hm") - .getAuthor(1).getFirst().orElse(null)); + .getAuthor(1).getGivenName().orElse(null)); } @Test public void parseLastNameFromFirstAuthorMultipleAuthorsWithLatexNames() throws Exception { assertEquals("al-Khw{\\={a}}rizm{\\={i}}", AuthorList.parse("Mu{\\d{h}}ammad al-Khw{\\={a}}rizm{\\={i}} and Corrado B{\\\"o}hm") - .getAuthor(0).getLast().orElse(null)); + .getAuthor(0).getFamilyName().orElse(null)); } @Test public void parseLastNameFromSecondAuthorMultipleAuthorsWithLatexNames() throws Exception { assertEquals("B{\\\"o}hm", AuthorList.parse("Mu{\\d{h}}ammad al-Khw{\\={a}}rizm{\\={i}} and Corrado B{\\\"o}hm") - .getAuthor(1).getLast().orElse(null)); + .getAuthor(1).getFamilyName().orElse(null)); } @Test public void parseInstitutionAuthorWithLatexNames() throws Exception { - assertEquals("The Ban\\={u} M\\={u}s\\={a} brothers", - AuthorList.parse("{The Ban\\={u} M\\={u}s\\={a} brothers}").getAuthor(0).getLast().orElse(null)); + assertEquals("{The Ban\\={u} M\\={u}s\\={a} brothers}", + AuthorList.parse("{The Ban\\={u} M\\={u}s\\={a} brothers}").getAuthor(0).getFamilyName().orElse(null)); } @Test diff --git a/src/test/java/org/jabref/model/entry/AuthorTest.java b/src/test/java/org/jabref/model/entry/AuthorTest.java index a1176bf2ac6..592f84f9923 100644 --- a/src/test/java/org/jabref/model/entry/AuthorTest.java +++ b/src/test/java/org/jabref/model/entry/AuthorTest.java @@ -1,5 +1,7 @@ package org.jabref.model.entry; +import java.util.Optional; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; @@ -70,6 +72,11 @@ void addDotIfAbbreviationEndsWithDoubleAbbreviation() { assertEquals("Ameli A. A.", Author.addDotIfAbbreviation("Ameli AA")); } + @Test + void bracesKept() { + assertEquals(Optional.of("{Company Name, LLC}"), new Author("", "", null, "{Company Name, LLC}", null).getFamilyName()); + } + @ParameterizedTest @ValueSource(strings = {"1", "1 23"}) void addDotIfAbbreviationIfStartsWithNumber(String input) { From 5ab2a81b282b96d013555baa3687f02e25616e41 Mon Sep 17 00:00:00 2001 From: Roc <1844478+ror3d@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:12:49 +0000 Subject: [PATCH 18/38] Improve citation relations (#11016) * Collect DOI and publication type from semantich scholar to be able to expand the information of the new entries later by search through DOI * Include abstract in the request. This lets the GUI show the abstract since that was implemented already. Refactor api request string since most of it is shared * Add button to open the relation paper's DOI URL. Fix DOI for some ArXiv entries. * Don't show the open link button if there is no link to open. * Make field value null error a bit more useful * Include SemanticScholar url in the request and use it as the URL field. * Add changes to changelog * Change tooltip text to an existing, more informative one * Run rewriter to fix pull request * improve url optional handling --------- Co-authored-by: Siedlerchr --- CHANGELOG.md | 1 + .../CitationRelationsTab.java | 34 ++++++- .../semanticscholar/PaperDetails.java | 90 +++++++++++++++++++ .../SemanticScholarFetcher.java | 23 ++--- .../java/org/jabref/model/entry/BibEntry.java | 2 +- 5 files changed, 130 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0334426d200..8c2dc50e8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,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) - We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) +- 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) ### Fixed diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index b3027a6f9b5..7f8fc40d15c 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -1,5 +1,7 @@ package org.jabref.gui.entryeditor.citationrelationtab; +import java.io.IOException; +import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -28,6 +30,7 @@ import org.jabref.gui.Globals; import org.jabref.gui.LibraryTab; import org.jabref.gui.StateManager; +import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.entryeditor.EntryEditorPreferences; import org.jabref.gui.entryeditor.EntryEditorTab; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; @@ -40,6 +43,9 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.identifier.DOI; +import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -201,6 +207,8 @@ private void styleFetchedListView(CheckListView listView) HBox hContainer = new HBox(); hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); + VBox vContainer = new VBox(); + if (entry.isLocal()) { Button jumpTo = IconTheme.JabRefIcons.LINK.asButton(); jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library"))); @@ -211,7 +219,7 @@ private void styleFetchedListView(CheckListView listView) citingTask.cancel(); citedByTask.cancel(); }); - hContainer.getChildren().addAll(entryNode, separator, jumpTo); + vContainer.getChildren().add(jumpTo); } else { ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton(); addToggle.setTooltip(new Tooltip(Localization.lang("Select entry"))); @@ -224,8 +232,28 @@ private void styleFetchedListView(CheckListView listView) }); addToggle.getStyleClass().add("addEntryButton"); addToggle.selectedProperty().bindBidirectional(listView.getItemBooleanProperty(entry)); - hContainer.getChildren().addAll(entryNode, separator, addToggle); + vContainer.getChildren().add(addToggle); + } + + if (entry.entry().getDOI().isPresent() || entry.entry().getField(StandardField.URL).isPresent()) { + Button openWeb = IconTheme.JabRefIcons.OPEN_LINK.asButton(); + openWeb.setTooltip(new Tooltip(Localization.lang("Open URL or DOI"))); + openWeb.setOnMouseClicked(event -> { + String url = entry.entry().getDOI().flatMap(DOI::getExternalURI).map(URI::toString) + .or(() -> entry.entry().getField(StandardField.URL)).orElse(""); + if (StringUtil.isNullOrEmpty(url)) { + return; + } + try { + JabRefDesktop.openBrowser(url, preferencesService.getFilePreferences()); + } catch (IOException ex) { + dialogService.notify(Localization.lang("Unable to open link.")); + } + }); + vContainer.getChildren().addLast(openWeb); } + + hContainer.getChildren().addAll(entryNode, separator, vContainer); hContainer.getStyleClass().add("entry-container"); return hContainer; @@ -392,8 +420,6 @@ private void showNodes(Node... nodes) { Arrays.stream(nodes).forEach(node -> node.setVisible(true)); } - // Absolute-phase phenomena in photoionization with few-cycle laser pulses - /** * Function to import selected entries to the database. Also writes the entries to import to the CITING/CITED field * diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java index 48db00777cf..58ba269616e 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java @@ -1,18 +1,28 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import com.google.gson.annotations.SerializedName; public class PaperDetails { private String paperId; private String title; private String year; + + @SerializedName("abstract") + private String abstr; + private String url; private int citationCount; private int referenceCount; private List authors; + private List publicationTypes; + private Map externalIds; public String getPaperId() { return paperId; @@ -38,6 +48,22 @@ public void setYear(String year) { this.year = year; } + public String getAbstract() { + return abstr; + } + + public void setAbstract(String abstr) { + this.abstr = abstr; + } + + public String getURL() { + return url; + } + + public void setURL(String url) { + this.url = url; + } + public int getCitationCount() { return citationCount; } @@ -58,6 +84,56 @@ public List getAuthors() { return authors; } + public String getPublicationType() { + if (publicationTypes == null || publicationTypes.isEmpty()) { + return "Misc"; + } + if (publicationTypes.contains("Conference")) { + return "InProceedings"; + } else if (publicationTypes.contains("JournalArticle")) { + return "Article"; + } else { + return switch (publicationTypes.getFirst()) { + case "Review" -> + "Misc"; + case "CaseReport" -> + "Report"; + case "ClinicalTrial" -> + "Report"; + case "Dataset" -> + "Dataset"; + case "Editorial" -> + "Misc"; + case "LettersAndComments" -> + "Misc"; + case "MetaAnalysis" -> + "Article"; + case "News" -> + "Misc"; + case "Study" -> + "Article"; + case "Book" -> + "Book"; + case "BookSection" -> + "InBook"; + default -> + "Misc"; + }; + } + } + + public String getDOI() { + if (externalIds != null) { + if (externalIds.containsKey("DOI")) { + return externalIds.get("DOI"); + } else if (externalIds.containsKey("ArXiv")) { + // Some ArXiv articles don't return the DOI, even though it's easy to obtain from the ArXiv ID + return "10.48550/arXiv." + externalIds.get("ArXiv"); + } + } + return ""; + } + public BibEntry toBibEntry() { BibEntry bibEntry = new BibEntry(); bibEntry.setField(StandardField.TITLE, getTitle()); @@ -70,6 +146,20 @@ public BibEntry toBibEntry() { .collect(Collectors.joining(" and ")); bibEntry.setField(StandardField.AUTHOR, authors); + bibEntry.setType(StandardEntryType.valueOf(getPublicationType())); + + if (getDOI() != null) { + bibEntry.setField(StandardField.DOI, getDOI()); + } + + if (getURL() != null) { + bibEntry.setField(StandardField.URL, getURL()); + } + + if (getAbstract() != null) { + bibEntry.setField(StandardField.ABSTRACT, getAbstract()); + } + return bibEntry; } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java index e263d27e12f..1463d22aa9c 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java @@ -25,17 +25,17 @@ public SemanticScholarFetcher(ImporterPreferences importerPreferences) { this.importerPreferences = importerPreferences; } + public String getAPIUrl(String entry_point, BibEntry entry) { + return SEMANTIC_SCHOLAR_API + "paper/" + "DOI:" + entry.getDOI().orElseThrow().getDOI() + "/" + entry_point + + "?fields=" + "title,authors,year,citationCount,referenceCount,externalIds,publicationTypes,abstract,url" + + "&limit=1000"; + } + @Override public List searchCitedBy(BibEntry entry) throws FetcherException { if (entry.getDOI().isPresent()) { - StringBuilder urlBuilder = new StringBuilder(SEMANTIC_SCHOLAR_API) - .append("paper/") - .append("DOI:").append(entry.getDOI().get().getDOI()) - .append("/citations") - .append("?fields=").append("title,authors,year,citationCount,referenceCount") - .append("&limit=1000"); try { - URL citationsUrl = URI.create(urlBuilder.toString()).toURL(); + URL citationsUrl = URI.create(getAPIUrl("citations", entry)).toURL(); URLDownload urlDownload = new URLDownload(citationsUrl); String apiKey = getApiKey(); @@ -58,15 +58,8 @@ public List searchCitedBy(BibEntry entry) throws FetcherException { @Override public List searchCiting(BibEntry entry) throws FetcherException { if (entry.getDOI().isPresent()) { - StringBuilder urlBuilder = new StringBuilder(SEMANTIC_SCHOLAR_API) - .append("paper/") - .append("DOI:").append(entry.getDOI().get().getDOI()) - .append("/references") - .append("?fields=") - .append("title,authors,year,citationCount,referenceCount") - .append("&limit=1000"); try { - URL referencesUrl = URI.create(urlBuilder.toString()).toURL(); + URL referencesUrl = URI.create(getAPIUrl("references", entry)).toURL(); URLDownload urlDownload = new URLDownload(referencesUrl); String apiKey = getApiKey(); if (!apiKey.isEmpty()) { diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 56fc9302796..3e26335a11e 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -602,7 +602,7 @@ public void setField(Map fields) { */ public Optional setField(Field field, String value, EntriesEventSource eventSource) { Objects.requireNonNull(field, "field name must not be null"); - Objects.requireNonNull(value, "field value must not be null"); + Objects.requireNonNull(value, "field value for field " + field.getName() + " must not be null"); Objects.requireNonNull(eventSource, "field eventSource must not be null"); if (value.isEmpty()) { From 7a269d4dac3c8a13cd97a1d9d0eaee29576f252d Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 11 Mar 2024 16:57:55 +0100 Subject: [PATCH 19/38] issue #10993 - doc: updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2dc50e8cd..4bf9d71418b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - Keywords filed are now displayed as tags. [#10910](https://github.com/JabRef/jabref/pull/10910) - We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) - 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) +- We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) ### Fixed From 008472b928972b1ae6e352a8fcc39e3d547c2d8b Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 18 Mar 2024 19:58:28 +0100 Subject: [PATCH 20/38] fix: fixed unit tests not passing due to name changes in Author interface (#10995) --- .../org/jabref/logic/exporter/CffExporter.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index cf2d7246102..23a450dc448 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -138,27 +138,27 @@ private void writeAuthors(AtomicFileWriter ps, List authors, boolean pre } for (Author author : authors) { boolean hyphen = false; - if (author.getLast().isPresent()) { + if (author.getFamilyName().isPresent()) { ps.write(pref ? " " : ""); - ps.write(" - family-names: " + author.getLast().get() + OS.NEWLINE); + ps.write(" - family-names: " + author.getFamilyName().get() + OS.NEWLINE); hyphen = true; } - if (author.getFirst().isPresent()) { + if (author.getGivenName().isPresent()) { ps.write(pref ? " " : ""); ps.write(hyphen ? " " : " - "); - ps.write("given-names: " + author.getFirst().get() + OS.NEWLINE); + ps.write("given-names: " + author.getGivenName().get() + OS.NEWLINE); hyphen = true; } - if (author.getVon().isPresent()) { + if (author.getNamePrefix().isPresent()) { ps.write(pref ? " " : ""); ps.write(hyphen ? " " : " - "); - ps.write("name-particle: " + author.getVon().get() + OS.NEWLINE); + ps.write("name-particle: " + author.getNamePrefix().get() + OS.NEWLINE); hyphen = true; } - if (author.getJr().isPresent()) { + if (author.getNameSuffix().isPresent()) { ps.write(pref ? " " : ""); ps.write(hyphen ? " " : " - "); - ps.write("name-suffix: " + author.getJr().get() + OS.NEWLINE); + ps.write("name-suffix: " + author.getNameSuffix().get() + OS.NEWLINE); } } } catch (IOException ex) { From 6f925ec9c426f07dfaf1885d0cc19508598cbc66 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 18 Mar 2024 22:11:54 +0100 Subject: [PATCH 21/38] feat: changed CFFExporter to use YAML library snakeyaml instead (#10995) --- build.gradle | 3 + src/main/java/module-info.java | 1 + .../jabref/logic/exporter/CffExporter.java | 193 ++++++++---------- .../logic/exporter/CffExporterTest.java | 78 +++---- 4 files changed, 123 insertions(+), 152 deletions(-) diff --git a/build.gradle b/build.gradle index 666c35f2336..fee9b1238af 100644 --- a/build.gradle +++ b/build.gradle @@ -245,6 +245,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/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/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 23a450dc448..72ae9c726bb 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -1,5 +1,6 @@ package org.jabref.logic.exporter; +import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -8,12 +9,13 @@ import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +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 org.jabref.logic.util.OS; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.Author; @@ -26,6 +28,9 @@ import org.jabref.model.entry.types.EntryType; import org.jabref.model.entry.types.StandardEntryType; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + /** * Exporter for exporting in CFF format. */ @@ -44,10 +49,25 @@ public void export(BibDatabaseContext databaseContext, Path file, List return; } - try (AtomicFileWriter ps = new AtomicFileWriter(file, StandardCharsets.UTF_8)) { - ps.write("# YAML 1.2" + OS.NEWLINE); - ps.write("---" + OS.NEWLINE); - ps.write("cff-version: 1.2.0" + OS.NEWLINE); + try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { + DumperOptions options = new DumperOptions(); + + // Set line width to infinity to avoid line wrapping + options.setWidth(Integer.MAX_VALUE); + + // Set collections to be written in block rather than inline + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + + // Set indent for sequences to two spaces + options.setIndentWithIndicator(true); + options.setIndicatorIndent(2); + Yaml yaml = new Yaml(options); + + Map originalData = new LinkedHashMap<>(); + Map preferredData = new LinkedHashMap<>(); + Map data = originalData; + data.put("cff-version", "1.2.0"); for (BibEntry entry : entries) { // Retrieve all fields @@ -56,36 +76,59 @@ public void export(BibDatabaseContext databaseContext, Path file, List // Compulsory message field String message = entryMap.getOrDefault(StandardField.COMMENT, "If you use this software, please cite it using the metadata from this file."); - ps.write("message: " + message + OS.NEWLINE); + data.put("message", message); entryMap.remove(StandardField.COMMENT); // Compulsory title field String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); - ps.write("title: " + "\"" + title + "\"" + OS.NEWLINE); + data.put("title", title); entryMap.remove(StandardField.TITLE); // Compulsory authors field List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) .getAuthors(); - writeAuthors(ps, authors, false); + + // Create two copies of the same list to avoid using YAML anchors and aliases + List> authorsList = new ArrayList<>(); + List> authorsListPreferred = new ArrayList<>(); + authors.forEach(author -> { + Map authorMap = new LinkedHashMap<>(); + Map authorMapPreferred = new LinkedHashMap<>(); + if (author.getFamilyName().isPresent()) { + authorMap.put("family-names", author.getFamilyName().get()); + authorMapPreferred.put("family-names", author.getFamilyName().get()); + } + if (author.getGivenName().isPresent()) { + authorMap.put("given-names", author.getGivenName().get()); + authorMapPreferred.put("given-names", author.getGivenName().get()); + } + if (author.getNamePrefix().isPresent()) { + authorMap.put("name-particle", author.getNamePrefix().get()); + authorMapPreferred.put("name-particle", author.getNamePrefix().get()); + } + if (author.getNameSuffix().isPresent()) { + authorMap.put("name-suffix", author.getNameSuffix().get()); + authorMapPreferred.put("name-suffix", author.getNameSuffix().get()); + } + authorsList.add(authorMap); + authorsListPreferred.add(authorMapPreferred); + }); + data.put("authors", authorsList.isEmpty() ? "No author specified." : authorsList); entryMap.remove(StandardField.AUTHOR); // Type Map typeMap = getTypeMappings(); EntryType entryType = entry.getType(); - boolean pref = false; switch (entryType) { - case StandardEntryType.Software -> - ps.write("type: software" + OS.NEWLINE); - case StandardEntryType.Dataset -> - ps.write("type: dataset" + OS.NEWLINE); + case StandardEntryType.Software, StandardEntryType.Dataset -> + data.put("type", entryType.getName()); default -> { if (typeMap.containsKey(entryType)) { - pref = true; - ps.write("preferred-citation:" + OS.NEWLINE); - ps.write(" type: " + typeMap.get(entryType) + OS.NEWLINE); - writeAuthors(ps, authors, true); - ps.write(" title: " + "\"" + title + "\"" + OS.NEWLINE); + data.put("preferred-citation", preferredData); + data = preferredData; + data.put("type", typeMap.get(entryType)); + data.put("authors", authorsListPreferred.isEmpty() ? "No author specified." : authorsListPreferred); + data.put("title", title); } } } @@ -93,19 +136,37 @@ public void export(BibDatabaseContext databaseContext, Path file, List // Keywords String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); if (keywords != null) { - ps.write(pref ? " " : ""); - ps.write("keywords:" + OS.NEWLINE); - for (String keyword : keywords.split(",\\s*")) { - ps.write(pref ? " " : ""); - ps.write(" - " + keyword + OS.NEWLINE); - } + data.put("keywords", keywords.split(",\\s*")); } entryMap.remove(StandardField.KEYWORDS); // Date String date = entryMap.getOrDefault(StandardField.DATE, null); if (date != null) { - writeDate(ps, date, pref); + String formatString; + try { + LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); + data.put("date-released", localDate.toString()); + } catch (DateTimeParseException e) { + try { + formatString = "yyyy-MM"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + YearMonth yearMonth = YearMonth.parse(date, formatter); + int month = yearMonth.getMonth().getValue(); + int year = yearMonth.getYear(); + data.put("month", month); + data.put("year", year); + } catch (DateTimeParseException f) { + try { + formatString = "yyyy"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + int year = Year.parse(date, formatter).getValue(); + data.put("year", year); + } catch (DateTimeParseException g) { + data.put("issue-date", date); + } + } + } } entryMap.remove(StandardField.DATE); @@ -113,90 +174,14 @@ public void export(BibDatabaseContext databaseContext, Path file, List Map fieldMap = getFieldMappings(); for (Field field : entryMap.keySet()) { if (fieldMap.containsKey(field)) { - ps.write(pref ? " " : ""); - ps.write(fieldMap.get(field) + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); + data.put(fieldMap.get(field), entryMap.get(field)); } else if (field instanceof UnknownField) { - ps.write(pref ? " " : ""); - ps.write(field.getName() + ": " + "\"" + entryMap.get(field) + "\"" + OS.NEWLINE); + data.put(field.getName(), entryMap.get(field)); } } } - } catch (IOException ex) { - throw new SaveException(ex); - } - } - - private void writeAuthors(AtomicFileWriter ps, List authors, boolean pref) throws Exception { - try { - ps.write(pref ? " " : ""); - ps.write("authors:"); - if (authors.isEmpty()) { - ps.write(pref ? " " : ""); - ps.write(" No author specified."); - } else { - ps.write(OS.NEWLINE); - } - for (Author author : authors) { - boolean hyphen = false; - if (author.getFamilyName().isPresent()) { - ps.write(pref ? " " : ""); - ps.write(" - family-names: " + author.getFamilyName().get() + OS.NEWLINE); - hyphen = true; - } - if (author.getGivenName().isPresent()) { - ps.write(pref ? " " : ""); - ps.write(hyphen ? " " : " - "); - ps.write("given-names: " + author.getGivenName().get() + OS.NEWLINE); - hyphen = true; - } - if (author.getNamePrefix().isPresent()) { - ps.write(pref ? " " : ""); - ps.write(hyphen ? " " : " - "); - ps.write("name-particle: " + author.getNamePrefix().get() + OS.NEWLINE); - hyphen = true; - } - if (author.getNameSuffix().isPresent()) { - ps.write(pref ? " " : ""); - ps.write(hyphen ? " " : " - "); - ps.write("name-suffix: " + author.getNameSuffix().get() + OS.NEWLINE); - } - } - } catch (IOException ex) { - throw new SaveException(ex); - } - } - private void writeDate(AtomicFileWriter ps, String dateField, boolean pref) throws Exception { - StringBuilder builder = new StringBuilder(); - String formatString = "yyyy-MM-dd"; - try { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - LocalDate date = LocalDate.parse(dateField, DateTimeFormatter.ISO_LOCAL_DATE); - builder.append(pref ? " " : "").append("date-released: ").append(date.format(formatter)); - } catch (DateTimeParseException e) { - if (pref) { - try { - formatString = "yyyy-MM"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - YearMonth yearMonth = YearMonth.parse(dateField, formatter); - int month = yearMonth.getMonth().getValue(); - int year = yearMonth.getYear(); - builder.append(" month: ").append(month).append(OS.NEWLINE); - builder.append(" year: ").append(year).append(OS.NEWLINE); - } catch (DateTimeParseException f) { - try { - formatString = "yyyy"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - int year = Year.parse(dateField, formatter).getValue(); - builder.append(" year: ").append(year).append(OS.NEWLINE); - } catch (DateTimeParseException g) { - builder.append(" issue-date: ").append(dateField).append(OS.NEWLINE); - } - } - } - } - try { - ps.write(builder.toString()); + yaml.dump(originalData, writer); } catch (IOException ex) { throw new SaveException(ex); } diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index f4f8ffd6c71..3abab83ac37 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -54,11 +54,9 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"Test Title\"", + "title: Test Title", "authors:", " - family-names: Author", " given-names: Test", @@ -67,8 +65,8 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception " authors:", " - family-names: Author", " given-names: Test", - " title: \"Test Title\"", - " url: \"http://example.com\""); + " title: Test Title", + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } @@ -85,11 +83,9 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"Test Title\"", + "title: Test Title", "authors:", " - family-names: Author", " given-names: Test", @@ -98,8 +94,8 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { " authors:", " - family-names: Author", " given-names: Test", - " title: \"Test Title\"", - " doi: \"random_doi_value\""); + " title: Test Title", + " doi: random_doi_value"); assertEquals(expected, Files.readAllLines(file)); } @@ -113,11 +109,9 @@ public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Except cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"No title specified.\"", + "title: No title specified.", "authors: No author specified." ); @@ -136,16 +130,14 @@ public final void exportsSoftwareCorrectly(@TempDir Path tempFile) throws Except cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"Test Title\"", + "title: Test Title", "authors:", " - family-names: Author", " given-names: Test", "type: software", - "doi: \"random_doi_value\""); + "doi: random_doi_value"); assertEquals(expected, Files.readAllLines(file)); } @@ -162,16 +154,14 @@ public final void exportsSoftwareDateCorrectly(@TempDir Path tempFile) throws Ex cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"Test Title\"", + "title: Test Title", "authors:", " - family-names: Author", " given-names: Test", "type: software", - "date-released: 2003-11-06"); + "date-released: '2003-11-06'"); assertEquals(expected, Files.readAllLines(file)); } @@ -188,11 +178,9 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"Test Title\"", + "title: Test Title", "authors:", " - family-names: Author", " given-names: Test", @@ -201,7 +189,7 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc " authors:", " - family-names: Author", " given-names: Test", - " title: \"Test Title\"", + " title: Test Title", " month: 11", " year: 2003"); @@ -221,11 +209,9 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception cffExporter.export(databaseContext, file, Collections.singletonList(entry)); List expected = List.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"細雪\"", + "title: 細雪", "authors:", " - family-names: 潤一郎", " given-names: 谷崎", @@ -234,8 +220,8 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception " authors:", " - family-names: 潤一郎", " given-names: 谷崎", - " title: \"細雪\"", - " url: \"http://example.com\""); + " title: 細雪", + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } @@ -260,11 +246,9 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { cffExporter.export(databaseContext, articleFile, Collections.singletonList(articleEntry)); Set expectedSoftware = Set.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"JabRef\"", + "title: JabRef", "authors:", " - family-names: Kopp", " given-names: Oliver", @@ -287,17 +271,15 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " - 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\""); + "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"); Set expectedArticle = Set.of( - "# YAML 1.2", - "---", "cff-version: 1.2.0", "message: If you use this software, please cite it using the metadata from this file.", - "title: \"JabRef: BibTeX-based literature management software\"", + "title: 'JabRef: BibTeX-based literature management software'", "authors:", " - family-names: Kopp", " given-names: Oliver", @@ -314,14 +296,14 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " given-names: Carl Christian", " - family-names: Schwentker", " given-names: Christoph", - " title: \"JabRef: BibTeX-based literature management software\"", - " month: \"11\"", - " issue: \"138\"", - " volume: \"44\"", - " year: \"2023\"", - " doi: \"10.47397/tb/44-3/tb138kopp-jabref\"", - " journal: \"TUGboat\"", - " number: \"3\""); + " title: 'JabRef: BibTeX-based literature management software'", + " month: '11'", + " issue: '138'", + " volume: '44'", + " year: '2023'", + " doi: 10.47397/tb/44-3/tb138kopp-jabref", + " journal: TUGboat", + " number: '3'"); // Tests equality of sets since last lines order is random and relies on entries internal order try (Stream st = Files.lines(softwareFile)) { From 5a60aff4f50874b58e40d17906f4a30dc329cdbe Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 18 Mar 2024 23:39:05 +0100 Subject: [PATCH 22/38] feat: added support for references and ALL possible CFF fields in importer (#10995) --- .../importer/fileformat/CffImporter.java | 174 ++++++++++++++---- .../importer/fileformat/CffImporterTest.java | 25 +++ .../fileformat/CffImporterReferences.cff | 47 +++++ 3 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff 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 13e4680e96e..182afc55136 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -53,13 +53,16 @@ private static class CffFormat { private final HashMap values = new HashMap<>(); @JsonProperty("authors") - private List authors; + private List authors; @JsonProperty("identifiers") private List ids; @JsonProperty("preferred-citation") - private CffPreferredCitation citation; + private CffReference citation; + + @JsonProperty("references") + private List references; @JsonProperty("keywords") private List keywords; @@ -73,10 +76,22 @@ 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 + private void setValues(String key, String value) { + values.put(key, value); + } + } + + private static class CffConference { + private final HashMap values = new HashMap<>(); + + public CffConference() { } @JsonAnySetter @@ -95,16 +110,55 @@ public CffIdentifier() { } } - private static class CffPreferredCitation { + private static class CffReference { private final HashMap values = new HashMap<>(); + @JsonProperty("authors") + private List authors; + + @JsonProperty("conference") + private CffConference 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; - @JsonProperty("authors") - private List authors; - - public CffPreferredCitation() { + public CffReference() { } @JsonAnySetter @@ -185,6 +239,8 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { for (Map.Entry property : citation.citation.values.entrySet()) { if (fieldMap.containsKey(property.getKey())) { preferredEntryMap.put(fieldMap.get(property.getKey()), property.getValue()); + } else if (getUnmappedFields().contains(property.getKey())) { + entryMap.put(new UnknownField(property.getKey()), property.getValue()); } } @@ -194,6 +250,24 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entriesList.add(preferredEntry); } + // Handle `references` field + if (citation.references != null) { + for (CffReference ref : citation.references) { + HashMap refEntryMap = new HashMap<>(); + EntryType refEntryType = typeMap.getOrDefault(ref.type, StandardEntryType.Article); + for (Map.Entry property : ref.values.entrySet()) { + if (fieldMap.containsKey(property.getKey())) { + refEntryMap.put(fieldMap.get(property.getKey()), property.getValue()); + } else if (getUnmappedFields().contains(property.getKey())) { + entryMap.put(new UnknownField(property.getKey()), property.getValue()); + } + } + refEntryMap.put(StandardField.AUTHOR, parseAuthors(ref.authors)); + BibEntry refEntry = new BibEntry(refEntryType); + refEntry.setField(refEntryMap); + entriesList.add(refEntry); + } + } return new ParserResult(entriesList); } @@ -213,27 +287,74 @@ 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("abstract", StandardField.ABSTRACT); + fieldMappings.put("date-released", StandardField.DATE); fieldMappings.put("doi", StandardField.DOI); + fieldMappings.put("keywords", StandardField.KEYWORDS); fieldMappings.put("license", BiblatexSoftwareField.LICENSE); + fieldMappings.put("message", StandardField.COMMENT); fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); + fieldMappings.put("title", StandardField.TITLE); 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); + fieldMappings.put("version", StandardField.VERSION); - // specific to preferred-citation - fieldMappings.put("month", StandardField.MONTH); - fieldMappings.put("year", StandardField.YEAR); - fieldMappings.put("journal", StandardField.JOURNAL); + // specific to references and preferred-citation + fieldMappings.put("edition", StandardField.EDITION); + fieldMappings.put("isbn", StandardField.ISBN); + fieldMappings.put("issn", StandardField.ISSN); fieldMappings.put("issue", StandardField.ISSUE); - fieldMappings.put("volume", StandardField.VOLUME); + fieldMappings.put("journal", StandardField.JOURNAL); + fieldMappings.put("month", StandardField.MONTH); + fieldMappings.put("notes", StandardField.NOTE); fieldMappings.put("number", StandardField.NUMBER); + fieldMappings.put("pages", StandardField.PAGES); + fieldMappings.put("status", StandardField.PUBSTATE); + fieldMappings.put("volume", StandardField.VOLUME); + fieldMappings.put("year", StandardField.YEAR); return fieldMappings; } + private List getUnmappedFields() { + List fields = new ArrayList<>(); + + fields.add("abbreviation"); + fields.add("collection-doi"); + fields.add("collection-title"); + fields.add("collection-type"); + fields.add("commit"); + fields.add("copyright"); + fields.add("data-type"); + fields.add("database"); + fields.add("date-accessed"); + fields.add("date-downloaded"); + fields.add("date-published"); + fields.add("department"); + fields.add("end"); + fields.add("entry"); + fields.add("filename"); + fields.add("format"); + fields.add("issue-date"); + fields.add("issue-title"); + fields.add("license-url"); + fields.add("loc-end"); + fields.add("loc-start"); + fields.add("medium"); + fields.add("nihmsid"); + fields.add("number-volumes"); + fields.add("patent-states"); + fields.add("pmcid"); + fields.add("repository-artifact"); + fields.add("repository-code"); + fields.add("scope"); + fields.add("section"); + fields.add("start"); + fields.add("term"); + fields.add("thesis-type"); + fields.add("volume-title"); + fields.add("year-original"); + return fields; + } + private HashMap getTypeMappings() { HashMap typeMappings = new HashMap<>(); typeMappings.put("article", StandardEntryType.Article); @@ -249,18 +370,7 @@ private HashMap getTypeMappings() { return typeMappings; } - private List getUnmappedFields() { - List fields = new ArrayList<>(); - - fields.add("commit"); - fields.add("license-url"); - fields.add("repository-code"); - fields.add("repository-artifact"); - - return fields; - } - - private String parseAuthors(List authors) { + private String parseAuthors(List authors) { return authors.stream() .map(author -> author.values) .map(vals -> vals.get("name") != null ? 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 b9462fbae9a..e7e204cb70c 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -163,6 +163,31 @@ public void importEntriesPreferredCitation() throws IOException, URISyntaxExcept 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(); + + BibEntry expectedMain = getPopulatedEntry(); + BibEntry expectedReference1 = new BibEntry(StandardEntryType.InProceedings); + expectedReference1.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); + expectedReference1.setField(StandardField.YEAR, "2007"); + expectedReference1.setField(StandardField.DOI, "10.0001/TEST"); + expectedReference1.setField(StandardField.URL, "www.example.com"); + + BibEntry expectedReference2 = new BibEntry(StandardEntryType.Manual); + expectedReference2.setField(StandardField.AUTHOR, "Arthur Clark, Jr. and Luca von Diamond"); + expectedReference2.setField(StandardField.DOI, "10.0002/TEST"); + expectedReference2.setField(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); 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..3b8e5490332 --- /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 \ No newline at end of file From 5e697a29adbefee69aae5ffa2bc37969e3e92b28 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Tue, 19 Mar 2024 15:09:58 +0100 Subject: [PATCH 23/38] fix: added requested changes (#10995) + updated CHANGELOG.md + removed useless comments + refactored both CffImporter and CffExporter to use more specific methods + used a BiMap to avoid repeating mappings between CffImporter and CffExporter + copied entryMap in exporter to avoid side-effects --- CHANGELOG.md | 6 +- .../jabref/logic/exporter/CffExporter.java | 346 +++++++++--------- .../importer/fileformat/CffImporter.java | 171 +++------ .../logic/exporter/CffExporterTest.java | 54 +-- .../importer/fileformat/CffImporterTest.java | 3 +- .../org/jabref/model/entry/BibEntryTest.java | 11 + .../fileformat/CffImporterReferences.cff | 2 +- 7 files changed, 274 insertions(+), 319 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d514f2b79..445223ee5f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,10 +49,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We enhanced the dialog for adding new fields in the content selector with a selection box containing a list of standard fields. [#10912](https://github.com/JabRef/jabref/pull/10912) - 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) -- We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) -- 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) -- We use another exporter for CFF format and the importer parses more fields. [#10993](https://github.com/JabRef/jabref/issues/10993) - 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 support JabRef `cites` and `related` relationships, as well as all fields from the CFF specification. [#10993](https://github.com/JabRef/jabref/issues/10993) ### Fixed @@ -72,7 +70,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the Document Viewer would show the PDF in only half the window when maximized. [#10934](https://github.com/JabRef/jabref/issues/10934) - Clicking on the crossref and related tags in the entry editor jumps to the linked entry. [#5484](https://github.com/JabRef/jabref/issues/5484) [#9369](https://github.com/JabRef/jabref/issues/9369) - We fixed an issue where JabRef could not parse absolute file paths from Zotero exports. [#10959](https://github.com/JabRef/jabref/issues/10959) -- We fixed an issue where an exception occured when toggling between "Live" or "Locked" in the internal Document Viewer. [#10935](https://github.com/JabRef/jabref/issues/10935) +- We fixed an issue where an exception occurred when toggling between "Live" or "Locked" in the internal Document Viewer. [#10935](https://github.com/JabRef/jabref/issues/10935) ### Removed diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 72ae9c726bb..a97e7470cc0 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -4,18 +4,19 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.time.LocalDate; import java.time.Year; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import org.jabref.logic.layout.format.DateFormatter; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.Author; @@ -31,10 +32,53 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; -/** - * Exporter for exporting in CFF format. - */ -class CffExporter extends Exporter { +public class CffExporter extends Exporter { + public static final List UNMAPPED_FIELDS = Arrays.asList( + "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); } @@ -49,179 +93,155 @@ public void export(BibDatabaseContext databaseContext, Path file, List return; } - try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { - DumperOptions options = new DumperOptions(); - - // Set line width to infinity to avoid line wrapping - options.setWidth(Integer.MAX_VALUE); - - // Set collections to be written in block rather than inline - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - options.setPrettyFlow(true); - - // Set indent for sequences to two spaces - options.setIndentWithIndicator(true); - options.setIndicatorIndent(2); - Yaml yaml = new Yaml(options); - - Map originalData = new LinkedHashMap<>(); - Map preferredData = new LinkedHashMap<>(); - Map data = originalData; - data.put("cff-version", "1.2.0"); - - for (BibEntry entry : entries) { - // Retrieve all fields - Map entryMap = entry.getFieldMap(); - - // Compulsory message field - String message = entryMap.getOrDefault(StandardField.COMMENT, - "If you use this software, please cite it using the metadata from this file."); - data.put("message", message); - entryMap.remove(StandardField.COMMENT); - - // Compulsory title field - String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); - data.put("title", title); - entryMap.remove(StandardField.TITLE); - - // Compulsory authors field - List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) - .getAuthors(); - - // Create two copies of the same list to avoid using YAML anchors and aliases - List> authorsList = new ArrayList<>(); - List> authorsListPreferred = new ArrayList<>(); - authors.forEach(author -> { - Map authorMap = new LinkedHashMap<>(); - Map authorMapPreferred = new LinkedHashMap<>(); - if (author.getFamilyName().isPresent()) { - authorMap.put("family-names", author.getFamilyName().get()); - authorMapPreferred.put("family-names", author.getFamilyName().get()); - } - if (author.getGivenName().isPresent()) { - authorMap.put("given-names", author.getGivenName().get()); - authorMapPreferred.put("given-names", author.getGivenName().get()); - } - if (author.getNamePrefix().isPresent()) { - authorMap.put("name-particle", author.getNamePrefix().get()); - authorMapPreferred.put("name-particle", author.getNamePrefix().get()); - } - if (author.getNameSuffix().isPresent()) { - authorMap.put("name-suffix", author.getNameSuffix().get()); - authorMapPreferred.put("name-suffix", author.getNameSuffix().get()); - } - authorsList.add(authorMap); - authorsListPreferred.add(authorMapPreferred); - }); - data.put("authors", authorsList.isEmpty() ? "No author specified." : authorsList); - entryMap.remove(StandardField.AUTHOR); - - // Type - Map typeMap = getTypeMappings(); - EntryType entryType = entry.getType(); - switch (entryType) { - case StandardEntryType.Software, StandardEntryType.Dataset -> - data.put("type", entryType.getName()); - default -> { - if (typeMap.containsKey(entryType)) { - data.put("preferred-citation", preferredData); - data = preferredData; - data.put("type", typeMap.get(entryType)); - data.put("authors", authorsListPreferred.isEmpty() ? "No author specified." : authorsListPreferred); - data.put("title", title); - } + DumperOptions options = new DumperOptions(); + + // Set line width to infinity to avoid line wrapping + options.setWidth(Integer.MAX_VALUE); + + // Set collections to be written in block rather than inline + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + + // Set indent for sequences to two spaces + options.setIndentWithIndicator(true); + options.setIndicatorIndent(2); + Yaml yaml = new Yaml(options); + + Map originalData = new LinkedHashMap<>(); + Map referencesData = new LinkedHashMap<>(); + Map data = originalData; + data.put("cff-version", "1.2.0"); + + for (BibEntry entry : entries) { + Map entryMap = new HashMap<>(entry.getFieldMap()); + + // Mandatory message field + String message = entryMap.getOrDefault(StandardField.COMMENT, + "If you use this software, please cite it using the metadata from this file."); + data.put("message", message); + entryMap.remove(StandardField.COMMENT); + + // Mandatory title field + String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); + data.put("title", title); + entryMap.remove(StandardField.TITLE); + + // Mandatory authors field + List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) + .getAuthors(); + List> authorsList = parseAuthors(data, authors); + entryMap.remove(StandardField.AUTHOR); + + // Type; + EntryType entryType = entry.getType(); + switch (entryType) { + case StandardEntryType.Software, StandardEntryType.Dataset -> + data.put("type", entryType.getName()); + default -> { + if (TYPES_MAP.containsKey(entryType)) { + data.put("references", referencesData); + data = referencesData; + data.put("type", TYPES_MAP.get(entryType)); + data.put("authors", authorsList.isEmpty() ? + "No author specified." : authorsList); + data.put("title", title); } } + } - // Keywords - String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); - if (keywords != null) { - data.put("keywords", keywords.split(",\\s*")); - } - entryMap.remove(StandardField.KEYWORDS); - - // Date - String date = entryMap.getOrDefault(StandardField.DATE, null); - if (date != null) { - String formatString; - try { - LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); - data.put("date-released", localDate.toString()); - } catch (DateTimeParseException e) { - try { - formatString = "yyyy-MM"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - YearMonth yearMonth = YearMonth.parse(date, formatter); - int month = yearMonth.getMonth().getValue(); - int year = yearMonth.getYear(); - data.put("month", month); - data.put("year", year); - } catch (DateTimeParseException f) { - try { - formatString = "yyyy"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - int year = Year.parse(date, formatter).getValue(); - data.put("year", year); - } catch (DateTimeParseException g) { - data.put("issue-date", date); - } - } - } - } - entryMap.remove(StandardField.DATE); - - // Fields - Map fieldMap = getFieldMappings(); - for (Field field : entryMap.keySet()) { - if (fieldMap.containsKey(field)) { - data.put(fieldMap.get(field), entryMap.get(field)); - } else if (field instanceof UnknownField) { + // Keywords + String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); + if (keywords != null) { + data.put("keywords", keywords.split(",\\s*")); + } + entryMap.remove(StandardField.KEYWORDS); + + // Date + String date = entryMap.getOrDefault(StandardField.DATE, null); + if (date != null) { + parseDate(data, date); + } + entryMap.remove(StandardField.DATE); + + // Fields + for (Field field : entryMap.keySet()) { + if (FIELDS_MAP.containsKey(field)) { + data.put(FIELDS_MAP.get(field), entryMap.get(field)); + } else if (field instanceof UnknownField) { + // Check that field is accepted by CFF format specification + if (UNMAPPED_FIELDS.contains(field.getName())) { data.put(field.getName(), entryMap.get(field)); } } } + } + try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { yaml.dump(originalData, writer); - } catch (IOException ex) { + } catch ( + IOException ex) { throw new SaveException(ex); } } - private Map getTypeMappings() { - Map typeMappings = new HashMap<>(); - typeMappings.put(StandardEntryType.Article, "article"); - typeMappings.put(StandardEntryType.Conference, "article"); - typeMappings.put(StandardEntryType.Book, "book"); - typeMappings.put(StandardEntryType.Booklet, "pamphlet"); - typeMappings.put(StandardEntryType.InProceedings, "conference-paper"); - typeMappings.put(StandardEntryType.Proceedings, "proceedings"); - typeMappings.put(StandardEntryType.Misc, "misc"); - typeMappings.put(StandardEntryType.Manual, "manual"); - typeMappings.put(StandardEntryType.Report, "report"); - typeMappings.put(StandardEntryType.TechReport, "report"); - typeMappings.put(StandardEntryType.Unpublished, "unpublished"); - return typeMappings; + private List> parseAuthors(Map data, List authors) { + List> authorsList = new ArrayList<>(); + // Copy the original list to avoid using YAML anchors and aliases; + List> authorsListCopy = new ArrayList<>(); + authors.forEach(author -> { + Map authorMap = new LinkedHashMap<>(); + Map authorMapCopy = new LinkedHashMap<>(); + if (author.getFamilyName().isPresent()) { + authorMap.put("family-names", author.getFamilyName().get()); + authorMapCopy.put("family-names", author.getFamilyName().get()); + } + if (author.getGivenName().isPresent()) { + authorMap.put("given-names", author.getGivenName().get()); + authorMapCopy.put("given-names", author.getGivenName().get()); + } + if (author.getNamePrefix().isPresent()) { + authorMap.put("name-particle", author.getNamePrefix().get()); + authorMapCopy.put("name-particle", author.getNamePrefix().get()); + } + if (author.getNameSuffix().isPresent()) { + authorMap.put("name-suffix", author.getNameSuffix().get()); + authorMapCopy.put("name-suffix", author.getNameSuffix().get()); + } + authorsList.add(authorMap); + authorsListCopy.add(authorMapCopy); + }); + data.put("authors", authorsList.isEmpty() ? "No author specified." : authorsList); + return authorsListCopy; } - private Map getFieldMappings() { - Map fieldMappings = new HashMap<>(); - fieldMappings.put(StandardField.TITLE, "title"); - fieldMappings.put(StandardField.VERSION, "version"); - fieldMappings.put(StandardField.DOI, "doi"); - fieldMappings.put(BiblatexSoftwareField.LICENSE, "license"); - fieldMappings.put(BiblatexSoftwareField.REPOSITORY, "repository"); - fieldMappings.put(StandardField.URL, "url"); - fieldMappings.put(StandardField.ABSTRACT, "abstract"); - fieldMappings.put(StandardField.COMMENT, "message"); - fieldMappings.put(StandardField.DATE, "date-released"); - fieldMappings.put(StandardField.KEYWORDS, "keywords"); - fieldMappings.put(StandardField.MONTH, "month"); - fieldMappings.put(StandardField.YEAR, "year"); - fieldMappings.put(StandardField.JOURNAL, "journal"); - fieldMappings.put(StandardField.ISSUE, "issue"); - fieldMappings.put(StandardField.VOLUME, "volume"); - fieldMappings.put(StandardField.NUMBER, "number"); - return fieldMappings; + private void parseDate(Map data, String date) { + String formatString; + try { + DateFormatter dateFormatter = new DateFormatter(); + data.put("date-released", dateFormatter.format(date)); + } catch ( + DateTimeParseException e) { + try { + formatString = "yyyy-MM"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + YearMonth yearMonth = YearMonth.parse(date, formatter); + int month = yearMonth.getMonth().getValue(); + int year = yearMonth.getYear(); + data.put("month", month); + data.put("year", year); + } catch ( + DateTimeParseException f) { + try { + formatString = "yyyy"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + int year = Year.parse(date, formatter).getValue(); + data.put("year", year); + } catch ( + DateTimeParseException g) { + data.put("issue-date", date); + } + } + } } } 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 182afc55136..89095bee84d 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; +import org.jabref.logic.exporter.CffExporter; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.util.StandardFileType; @@ -24,9 +25,13 @@ 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(); + @Override public String getName() { return "CFF"; @@ -44,8 +49,7 @@ public String getId() { @Override public String getDescription() { - return "Importer for the CFF format. Is only used to cite software, one entry per file. Can also " + - "cite a preferred citation."; + return "Importer for the CFF format, which is intended to make software and datasets citable."; } // POJO classes for yaml data @@ -173,19 +177,19 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { CffFormat citation = mapper.readValue(reader, CffFormat.class); List entriesList = new ArrayList<>(); - // Retrieve mappings from CFF to JabRef - HashMap fieldMap = getFieldMappings(); - HashMap typeMap = getTypeMappings(); + // Remove CFF version and type + citation.values.remove("cff-version"); // Parse main entry HashMap entryMap = new HashMap<>(); - EntryType entryType = typeMap.getOrDefault(citation.values.get("type"), StandardEntryType.Software); + EntryType entryType = TYPES_MAP.getOrDefault(citation.values.get("type"), StandardEntryType.Software); + citation.values.remove("type"); // Map CFF fields to JabRef Fields for (Map.Entry property : citation.values.entrySet()) { - if (fieldMap.containsKey(property.getKey())) { - entryMap.put(fieldMap.get(property.getKey()), property.getValue()); - } else if (getUnmappedFields().contains(property.getKey())) { + if (FIELDS_MAP.containsKey(property.getKey())) { + entryMap.put(FIELDS_MAP.get(property.getKey()), property.getValue()); + } else { entryMap.put(new UnknownField(property.getKey()), property.getValue()); } } @@ -232,42 +236,14 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entry.setField(entryMap); entriesList.add(entry); - // Handle `preferred-citation` field if (citation.citation != null) { - HashMap preferredEntryMap = new HashMap<>(); - EntryType preferredEntryType = typeMap.getOrDefault(citation.citation.type, StandardEntryType.Article); - for (Map.Entry property : citation.citation.values.entrySet()) { - if (fieldMap.containsKey(property.getKey())) { - preferredEntryMap.put(fieldMap.get(property.getKey()), property.getValue()); - } else if (getUnmappedFields().contains(property.getKey())) { - entryMap.put(new UnknownField(property.getKey()), property.getValue()); - } - } - - preferredEntryMap.put(StandardField.AUTHOR, parseAuthors(citation.citation.authors)); - BibEntry preferredEntry = new BibEntry(preferredEntryType); - preferredEntry.setField(preferredEntryMap); - entriesList.add(preferredEntry); + entriesList.add(parsePreferredCitation(citation.citation)); } - // Handle `references` field if (citation.references != null) { - for (CffReference ref : citation.references) { - HashMap refEntryMap = new HashMap<>(); - EntryType refEntryType = typeMap.getOrDefault(ref.type, StandardEntryType.Article); - for (Map.Entry property : ref.values.entrySet()) { - if (fieldMap.containsKey(property.getKey())) { - refEntryMap.put(fieldMap.get(property.getKey()), property.getValue()); - } else if (getUnmappedFields().contains(property.getKey())) { - entryMap.put(new UnknownField(property.getKey()), property.getValue()); - } - } - refEntryMap.put(StandardField.AUTHOR, parseAuthors(ref.authors)); - BibEntry refEntry = new BibEntry(refEntryType); - refEntry.setField(refEntryMap); - entriesList.add(refEntry); - } + entriesList.addAll(parseReferences(citation.references)); } + return new ParserResult(entriesList); } @@ -285,91 +261,6 @@ public boolean isRecognizedFormat(BufferedReader reader) throws IOException { } } - private HashMap getFieldMappings() { - HashMap fieldMappings = new HashMap<>(); - fieldMappings.put("abstract", StandardField.ABSTRACT); - fieldMappings.put("date-released", StandardField.DATE); - fieldMappings.put("doi", StandardField.DOI); - fieldMappings.put("keywords", StandardField.KEYWORDS); - fieldMappings.put("license", BiblatexSoftwareField.LICENSE); - fieldMappings.put("message", StandardField.COMMENT); - fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); - fieldMappings.put("title", StandardField.TITLE); - fieldMappings.put("url", StandardField.URL); - fieldMappings.put("version", StandardField.VERSION); - - // specific to references and preferred-citation - fieldMappings.put("edition", StandardField.EDITION); - fieldMappings.put("isbn", StandardField.ISBN); - fieldMappings.put("issn", StandardField.ISSN); - fieldMappings.put("issue", StandardField.ISSUE); - fieldMappings.put("journal", StandardField.JOURNAL); - fieldMappings.put("month", StandardField.MONTH); - fieldMappings.put("notes", StandardField.NOTE); - fieldMappings.put("number", StandardField.NUMBER); - fieldMappings.put("pages", StandardField.PAGES); - fieldMappings.put("status", StandardField.PUBSTATE); - fieldMappings.put("volume", StandardField.VOLUME); - fieldMappings.put("year", StandardField.YEAR); - return fieldMappings; - } - - private List getUnmappedFields() { - List fields = new ArrayList<>(); - - fields.add("abbreviation"); - fields.add("collection-doi"); - fields.add("collection-title"); - fields.add("collection-type"); - fields.add("commit"); - fields.add("copyright"); - fields.add("data-type"); - fields.add("database"); - fields.add("date-accessed"); - fields.add("date-downloaded"); - fields.add("date-published"); - fields.add("department"); - fields.add("end"); - fields.add("entry"); - fields.add("filename"); - fields.add("format"); - fields.add("issue-date"); - fields.add("issue-title"); - fields.add("license-url"); - fields.add("loc-end"); - fields.add("loc-start"); - fields.add("medium"); - fields.add("nihmsid"); - fields.add("number-volumes"); - fields.add("patent-states"); - fields.add("pmcid"); - fields.add("repository-artifact"); - fields.add("repository-code"); - fields.add("scope"); - fields.add("section"); - fields.add("start"); - fields.add("term"); - fields.add("thesis-type"); - fields.add("volume-title"); - fields.add("year-original"); - return fields; - } - - private HashMap getTypeMappings() { - HashMap typeMappings = new HashMap<>(); - typeMappings.put("article", StandardEntryType.Article); - typeMappings.put("book", StandardEntryType.Book); - typeMappings.put("pamphlet", StandardEntryType.Booklet); - typeMappings.put("conference-paper", StandardEntryType.InProceedings); - typeMappings.put("misc", StandardEntryType.Misc); - typeMappings.put("manual", StandardEntryType.Manual); - typeMappings.put("software", StandardEntryType.Software); - typeMappings.put("dataset", StandardEntryType.Dataset); - typeMappings.put("report", StandardEntryType.Report); - typeMappings.put("unpublished", StandardEntryType.Unpublished); - return typeMappings; - } - private String parseAuthors(List authors) { return authors.stream() .map(author -> author.values) @@ -380,4 +271,34 @@ private String parseAuthors(List authors) { .collect(AuthorList.collect()) .getAsFirstLastNamesWithAnd(); } + + private BibEntry parsePreferredCitation(CffReference preferred) { + // TODO add `cites` relation to main entry + return parseEntry(preferred); + } + + private List parseReferences(List references) { + // TODO add `related` relation to main entry + List refEntries = new ArrayList<>(); + for (CffReference ref : references) { + refEntries.add(parseEntry(ref)); + } + return refEntries; + } + + private BibEntry parseEntry(CffReference reference) { + HashMap entryMap = new HashMap<>(); + EntryType entryType = TYPES_MAP.getOrDefault(reference.type, StandardEntryType.Article); + for (Map.Entry property : reference.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()); + } + } + entryMap.put(StandardField.AUTHOR, parseAuthors(reference.authors)); + BibEntry entry = new BibEntry(entryType); + entry.setField(entryMap); + return entry; + } } diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index 3abab83ac37..15c1d18ee95 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -34,22 +34,22 @@ static void setUp() { } @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)); @@ -60,7 +60,7 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception "authors:", " - family-names: Author", " given-names: Test", - "preferred-citation:", + "references:", " type: article", " authors:", " - family-names: Author", @@ -72,13 +72,14 @@ public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception } @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)); @@ -89,7 +90,7 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { "authors:", " - family-names: Author", " given-names: Test", - "preferred-citation:", + "references:", " type: conference-paper", " authors:", " - family-names: Author", @@ -101,10 +102,10 @@ public final void usesCorrectType(@TempDir Path tempFile) throws Exception { } @Test - public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Exception { - BibEntry entry = new BibEntry(StandardEntryType.Thesis); + public final void usesCorrectDefaultValues(@TempDir Path tempDir) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Thesis).withCitationKey("test"); - Path file = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); @@ -119,13 +120,14 @@ public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Except } @Test - public final void exportsSoftwareCorrectly(@TempDir Path tempFile) throws Exception { + 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 = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); @@ -143,13 +145,14 @@ public final void exportsSoftwareCorrectly(@TempDir Path tempFile) throws Except } @Test - public final void exportsSoftwareDateCorrectly(@TempDir Path tempFile) throws Exception { + 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)); @@ -167,13 +170,14 @@ public final void exportsSoftwareDateCorrectly(@TempDir Path tempFile) throws Ex } @Test - public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exception { + 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 = tempFile.resolve("RandomFileName"); + Path file = tempDir.resolve("RandomFileName"); Files.createFile(file); cffExporter.export(databaseContext, file, Collections.singletonList(entry)); @@ -184,7 +188,7 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc "authors:", " - family-names: Author", " given-names: Test", - "preferred-citation:", + "references:", " type: article", " authors:", " - family-names: Author", @@ -197,14 +201,14 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempFile) throws Exc } @Test - public final 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)); @@ -215,7 +219,7 @@ public final void passesModifiedCharset(@TempDir Path tempFile) throws Exception "authors:", " - family-names: 潤一郎", " given-names: 谷崎", - "preferred-citation:", + "references:", " type: article", " authors:", " - family-names: 潤一郎", @@ -287,7 +291,7 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " given-names: Carl Christian", " - family-names: Schwentker", " given-names: Christoph", - "preferred-citation:", + "references:", " type: article", " authors:", " - family-names: Kopp", @@ -303,7 +307,9 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " year: '2023'", " doi: 10.47397/tb/44-3/tb138kopp-jabref", " journal: TUGboat", - " number: '3'"); + " number: '3'", + " start: '441'", + " end: '447'"); // Tests equality of sets since last lines order is random and relies on entries internal order try (Stream st = Files.lines(softwareFile)) { 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 e7e204cb70c..e8a17b9cdc3 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -46,8 +46,7 @@ public void sGetExtensions() { @Test public void getDescription() { - assertEquals("Importer for the CFF format. Is only used to cite software, one entry per file. " + - "Can also cite a preferred citation.", + assertEquals("Importer for the CFF format, which is intended to make software and datasets citable.", importer.getDescription()); } diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java index 25ed76d7633..11eaeb9bc4a 100644 --- a/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -87,6 +88,16 @@ void setFieldWorksWithBibFieldAsWell() throws Exception { assertEquals(Optional.of("value"), entry.getField(StandardField.AUTHOR)); } + @Test + void shallowCopiedBibEntryDoesNotRemoveFields() throws Exception { + entry.setField(StandardField.URL, "value"); + Map entryMap = entry.getFieldMap(); + Map entryMapCopy = new HashMap<>(entryMap); + entryMapCopy.remove(StandardField.URL); + assertEquals(entryMapCopy.size(), 0); + assertEquals(entryMap.get(StandardField.URL), "value"); + } + @Test void clonedBibEntryHasUniqueID() throws Exception { BibEntry entryClone = (BibEntry) entry.clone(); diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff index 3b8e5490332..dee78017ab4 100644 --- a/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff +++ b/src/test/resources/org/jabref/logic/importer/fileformat/CffImporterReferences.cff @@ -44,4 +44,4 @@ references: given-names: Luca name-particle: von doi: 10.0002/TEST - url: www.facebook.com \ No newline at end of file + url: www.facebook.com From 88c42b8db000b3df1f782e1e6d7742f6367eb378 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Tue, 19 Mar 2024 15:34:59 +0100 Subject: [PATCH 24/38] fix: task rewriteDryRun fixed to pass by removing test in BibEntryTest --- .../java/org/jabref/model/entry/BibEntryTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java index 11eaeb9bc4a..25ed76d7633 100644 --- a/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -88,16 +87,6 @@ void setFieldWorksWithBibFieldAsWell() throws Exception { assertEquals(Optional.of("value"), entry.getField(StandardField.AUTHOR)); } - @Test - void shallowCopiedBibEntryDoesNotRemoveFields() throws Exception { - entry.setField(StandardField.URL, "value"); - Map entryMap = entry.getFieldMap(); - Map entryMapCopy = new HashMap<>(entryMap); - entryMapCopy.remove(StandardField.URL); - assertEquals(entryMapCopy.size(), 0); - assertEquals(entryMap.get(StandardField.URL), "value"); - } - @Test void clonedBibEntryHasUniqueID() throws Exception { BibEntry entryClone = (BibEntry) entry.clone(); From 927136888c0cbc2d25cd150cbeec22b4196407ee Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Tue, 19 Mar 2024 16:01:29 +0100 Subject: [PATCH 25/38] refactor: deleted useless methods in CffImporter (#10995) --- .../importer/fileformat/CffImporter.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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 89095bee84d..8947afe67cb 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -237,11 +237,15 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entriesList.add(entry); if (citation.citation != null) { - entriesList.add(parsePreferredCitation(citation.citation)); + // TODO add `preferred-citation` relation from main entry + entriesList.add(parseEntry(citation.citation)); } if (citation.references != null) { - entriesList.addAll(parseReferences(citation.references)); + // TODO add `related` relation from main entry + for (CffReference ref : citation.references) { + entriesList.add(parseEntry(ref)); + } } return new ParserResult(entriesList); @@ -272,20 +276,6 @@ private String parseAuthors(List authors) { .getAsFirstLastNamesWithAnd(); } - private BibEntry parsePreferredCitation(CffReference preferred) { - // TODO add `cites` relation to main entry - return parseEntry(preferred); - } - - private List parseReferences(List references) { - // TODO add `related` relation to main entry - List refEntries = new ArrayList<>(); - for (CffReference ref : references) { - refEntries.add(parseEntry(ref)); - } - return refEntries; - } - private BibEntry parseEntry(CffReference reference) { HashMap entryMap = new HashMap<>(); EntryType entryType = TYPES_MAP.getOrDefault(reference.type, StandardEntryType.Article); From 69245be1f4d2365c042724a12fc410cfb70c5949 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Tue, 19 Mar 2024 17:18:19 +0100 Subject: [PATCH 26/38] doc: added decision MADR document for cff export (#10995) --- .../0029-cff-export-multiple-entries.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/decisions/0029-cff-export-multiple-entries.md 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..55e0f837fb5 --- /dev/null +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -0,0 +1,60 @@ +--- +nav_order: 29 +parent: Decision Records +status: {'accepted'} +date: {2024-03-19 when the decision was last updated} +--- + + + +# 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` element as `preferred-citation` + * Export non-`software` entries with dummy topmost `sofware` element as `references` + * Forbid exporting multiple entries 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 of the `software` elements 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 +* `cites` and `related` fields of non-`software` entries are not supported + + From ad2d6001fcda4a66a56737383cf2480cb3c9350f Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Wed, 20 Mar 2024 15:26:14 +0100 Subject: [PATCH 27/38] feat: add a cites or related relationship between imported entries in CffImporter (#10995) --- .../org/jabref/cli/ArgumentProcessor.java | 3 +- src/main/java/org/jabref/cli/JabRefCLI.java | 3 +- .../gui/externalfiles/ImportHandler.java | 3 +- .../jabref/gui/importer/ImportCommand.java | 6 +- .../logic/importer/ImportFormatReader.java | 8 +- .../importer/fileformat/CffImporter.java | 94 +++++++++++-------- .../logic/exporter/CffExporterTest.java | 4 +- .../ImportFormatReaderIntegrationTest.java | 4 +- .../ImportFormatReaderParameterlessTest.java | 3 +- .../importer/fileformat/CffImporterTest.java | 22 ++++- 10 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index d8272507450..0e43846ed8f 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -166,7 +166,8 @@ private Optional importFile(Path file, String importFormat) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + fileUpdateMonitor, + preferencesService.getCitationKeyPatternPreferences()); 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..96eba9a143e 100644 --- a/src/main/java/org/jabref/cli/JabRefCLI.java +++ b/src/main/java/org/jabref/cli/JabRefCLI.java @@ -317,7 +317,8 @@ public static void printUsage(PreferencesService preferencesService) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - new DummyFileUpdateMonitor()); + new DummyFileUpdateMonitor(), + preferencesService.getCitationKeyPatternPreferences()); 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..131d93c809e 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -372,7 +372,8 @@ private List tryImportFormats(String data) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + fileUpdateMonitor, + preferencesService.getCitationKeyPatternPreferences()); 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..8654fbcf959 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCommand.java +++ b/src/main/java/org/jabref/gui/importer/ImportCommand.java @@ -79,7 +79,8 @@ public void execute() { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + fileUpdateMonitor, + preferencesService.getCitationKeyPatternPreferences()); SortedSet importers = importFormatReader.getImportFormats(); FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() @@ -134,7 +135,8 @@ private ParserResult doImport(List files, Importer importFormat) throws IO ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor); + fileUpdateMonitor, + preferencesService.getCitationKeyPatternPreferences()); for (Path filename : files) { try { if (importer.isEmpty()) { diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index c7eb32f0d38..7173aadd4e0 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, - FileUpdateMonitor fileUpdateMonitor) { + FileUpdateMonitor fileUpdateMonitor, + CitationKeyPatternPreferences citationKeyPatternPreferences) { 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 8947afe67cb..3162de41e92 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.Map; +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; @@ -32,6 +34,12 @@ 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"; @@ -62,15 +70,15 @@ private static class CffFormat { @JsonProperty("identifiers") private List ids; + @JsonProperty("keywords") + private List keywords; + @JsonProperty("preferred-citation") - private CffReference citation; + private CffReference preferred; @JsonProperty("references") private List references; - @JsonProperty("keywords") - private List keywords; - public CffFormat() { } @@ -92,18 +100,6 @@ private void setValues(String key, String value) { } } - private static class CffConference { - private final HashMap values = new HashMap<>(); - - public CffConference() { - } - - @JsonAnySetter - private void setValues(String key, String value) { - values.put(key, value); - } - } - private static class CffIdentifier { @JsonProperty("type") private String type; @@ -121,7 +117,7 @@ private static class CffReference { private List authors; @JsonProperty("conference") - private CffConference conference; + private CffEntity conference; @JsonProperty("contact") private CffEntity contact; @@ -185,22 +181,16 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { EntryType entryType = TYPES_MAP.getOrDefault(citation.values.get("type"), StandardEntryType.Software); citation.values.remove("type"); - // Map CFF fields to JabRef Fields - for (Map.Entry property : citation.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()); - } - } + // Translate CFF author format to JabRef author format + entryMap.put(StandardField.AUTHOR, parseAuthors(citation.authors)); // Parse keywords if (citation.keywords != null) { entryMap.put(StandardField.KEYWORDS, String.join(", ", citation.keywords)); } - // Translate CFF author format to JabRef author format - entryMap.put(StandardField.AUTHOR, parseAuthors(citation.authors)); + // Map CFF simple fields to JabRef Fields + parseFields(citation.values, entryMap); // Select DOI to keep if ((entryMap.get(StandardField.DOI) == null) && (citation.ids != null)) { @@ -236,19 +226,37 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entry.setField(entryMap); entriesList.add(entry); - if (citation.citation != null) { - // TODO add `preferred-citation` relation from main entry - entriesList.add(parseEntry(citation.citation)); + // 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) { - // TODO add `related` relation from main entry - for (CffReference ref : citation.references) { - entriesList.add(parseEntry(ref)); - } + 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("")); } - return new ParserResult(entriesList); + 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 @@ -277,18 +285,22 @@ private String parseAuthors(List authors) { } private BibEntry parseEntry(CffReference reference) { - HashMap entryMap = new HashMap<>(); + Map entryMap = new HashMap<>(); EntryType entryType = TYPES_MAP.getOrDefault(reference.type, StandardEntryType.Article); - for (Map.Entry property : reference.values.entrySet()) { + entryMap.put(StandardField.AUTHOR, parseAuthors(reference.authors)); + parseFields(reference.values, entryMap); + BibEntry entry = new BibEntry(entryType); + entry.setField(entryMap); + return entry; + } + + 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()); } } - entryMap.put(StandardField.AUTHOR, parseAuthors(reference.authors)); - BibEntry entry = new BibEntry(entryType); - entry.setField(entryMap); - return entry; } } diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index 15c1d18ee95..fcb103bddfd 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.logic.importer.fileformat.CffImporter; import org.jabref.logic.importer.fileformat.CffImporterTest; import org.jabref.model.database.BibDatabaseContext; @@ -21,6 +22,7 @@ import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; public class CffExporterTest { @@ -234,7 +236,7 @@ public final void passesModifiedCharset(@TempDir Path tempDir) throws Exception public final void roundTripTest(@TempDir Path tempDir) throws Exception { // First, import the file which will be parsed as two entries - CffImporter importer = new CffImporter(); + CffImporter importer = new CffImporter(mock(CitationKeyPatternPreferences.class)); Path file = Path.of(CffImporterTest.class.getResource("CITATION.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry softwareEntry = bibEntries.getFirst(); diff --git a/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java b/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java index 0fa6623fe6c..64045daac54 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()); + new DummyFileUpdateMonitor(), + mock(CitationKeyPatternPreferences.class)); } @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..4858f7c5b11 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, fileMonitor, mock(CitationKeyPatternPreferences.class)); } @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 e8a17b9cdc3..25b6d49ab3a 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,9 @@ 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 @@ -151,9 +158,12 @@ public void importEntriesPreferredCitation() throws IOException, URISyntaxExcept BibEntry mainEntry = bibEntries.getFirst(); BibEntry preferredEntry = bibEntries.getLast(); + String citeKey = preferredEntry.getCitationKey().orElse(""); BibEntry expectedMain = getPopulatedEntry(); - BibEntry expectedPreferred = new BibEntry(StandardEntryType.InProceedings); + expectedMain.setField(StandardField.CITES, citeKey); + + BibEntry expectedPreferred = new BibEntry(StandardEntryType.InProceedings).withCitationKey(citeKey); expectedPreferred.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); expectedPreferred.setField(StandardField.DOI, "10.0001/TEST"); expectedPreferred.setField(StandardField.URL, "www.github.com"); @@ -169,15 +179,19 @@ public void importEntriesReferences() throws IOException, URISyntaxException { 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(); - BibEntry expectedReference1 = new BibEntry(StandardEntryType.InProceedings); + expectedMain.setField(StandardField.RELATED, citeKey1 + ", " + citeKey2); + + BibEntry expectedReference1 = new BibEntry(StandardEntryType.InProceedings).withCitationKey(citeKey1); expectedReference1.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); expectedReference1.setField(StandardField.YEAR, "2007"); expectedReference1.setField(StandardField.DOI, "10.0001/TEST"); expectedReference1.setField(StandardField.URL, "www.example.com"); - BibEntry expectedReference2 = new BibEntry(StandardEntryType.Manual); + BibEntry expectedReference2 = new BibEntry(StandardEntryType.Manual).withCitationKey(citeKey2); expectedReference2.setField(StandardField.AUTHOR, "Arthur Clark, Jr. and Luca von Diamond"); expectedReference2.setField(StandardField.DOI, "10.0002/TEST"); expectedReference2.setField(StandardField.URL, "www.facebook.com"); From ca9c0dcba49d2211acaf73ea05dda740d117d661 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Wed, 20 Mar 2024 15:58:31 +0100 Subject: [PATCH 28/38] doc: updated MADR decision document for cff export to pass markdownlint (#10995) --- .../0029-cff-export-multiple-entries.md | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/docs/decisions/0029-cff-export-multiple-entries.md b/docs/decisions/0029-cff-export-multiple-entries.md index 55e0f837fb5..aef8f31a8e7 100644 --- a/docs/decisions/0029-cff-export-multiple-entries.md +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -1,17 +1,22 @@ --- -nav_order: 29 +# Replace this with the number of the ADR +nav_order: 28 + +# Keep this parent: Decision Records + +# These are optional elements. Feel free to remove any of them. status: {'accepted'} date: {2024-03-19 when the decision was last updated} --- -# Exporting multiple entries to CFF +# 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. +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 @@ -21,34 +26,35 @@ The need for an [exporter](https://github.com/JabRef/jabref/issues/10661) to [CF ## Considered Options -* When exporting : - * Export non-`software` entries with dummy topmost `sofware` element as `preferred-citation` - * Export non-`software` entries with dummy topmost `sofware` element as `references` - * Forbid exporting multiple entries 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 exporting : + * Export non-`software` entries with dummy topmost `sofware` element as `preferred-citation` + * Export non-`software` entries with dummy topmost `sofware` element as `references` + * Forbid exporting multiple entries 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` + * 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 of the `software` elements 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. +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` +* 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 @@ -56,5 +62,3 @@ The decision outcome is the following. * Importing a CFF file and then exporting one of the preferred citation or the references created entries won't result in the same file * `cites` and `related` fields of non-`software` entries are not supported - - From 359237d1c2574b84cee244d6473c5a9aee57f28e Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Wed, 20 Mar 2024 16:05:29 +0100 Subject: [PATCH 29/38] fix: fixed round-trip test to use mock citatioKeyPatternPreferences correctly (#10995) --- .../java/org/jabref/logic/exporter/CffExporterTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index fcb103bddfd..d627a2e8e0b 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; 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.BibDatabaseContext; @@ -20,9 +21,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CffExporterTest { @@ -236,7 +239,9 @@ public final void passesModifiedCharset(@TempDir Path tempDir) throws Exception public final void roundTripTest(@TempDir Path tempDir) throws Exception { // First, import the file which will be parsed as two entries - CffImporter importer = new CffImporter(mock(CitationKeyPatternPreferences.class)); + CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class, Answers.RETURNS_SMART_NULLS); + when(citationKeyPatternPreferences.getKeyPattern()).thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); + CffImporter importer = new CffImporter(citationKeyPatternPreferences); Path file = Path.of(CffImporterTest.class.getResource("CITATION.cff").toURI()); List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); BibEntry softwareEntry = bibEntries.getFirst(); From a8518b7800b3af7060d3a2c52163b60a9b234916 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Wed, 20 Mar 2024 16:09:39 +0100 Subject: [PATCH 30/38] fix: fixed MADR document for CFF export decision to pass Jekyll CI check (#10995) --- docs/decisions/0029-cff-export-multiple-entries.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/decisions/0029-cff-export-multiple-entries.md b/docs/decisions/0029-cff-export-multiple-entries.md index aef8f31a8e7..8814e28a4d8 100644 --- a/docs/decisions/0029-cff-export-multiple-entries.md +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -1,13 +1,6 @@ --- -# Replace this with the number of the ADR nav_order: 28 - -# Keep this parent: Decision Records - -# These are optional elements. Feel free to remove any of them. -status: {'accepted'} -date: {2024-03-19 when the decision was last updated} --- From 0264c03270d4c9ceccbbe8addcd4463de000bba4 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Wed, 20 Mar 2024 16:24:12 +0100 Subject: [PATCH 31/38] fix: fixed requested changes (#10995) + fixed typo in CHANGELOG.md + tested multiline abstract in CFFImporter --- CHANGELOG.md | 2 +- .../importer/fileformat/CffImporterTest.java | 25 +++++++++++++ .../CffImporterTestMultilineAbstract.cff | 36 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/org/jabref/logic/importer/fileformat/CffImporterTestMultilineAbstract.cff diff --git a/CHANGELOG.md b/CHANGELOG.md index 75494898aa8..8f3fae0d90d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +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 support JabRef `cites` and `related` relationships, as well as all fields from the CFF specification. [#10993](https://github.com/JabRef/jabref/issues/10993) +- 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/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java index 25b6d49ab3a..31ff9439a23 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -151,6 +151,31 @@ public void importEntriesUnknownFields() throws IOException, URISyntaxException assertEquals(entry, expected); } + @Test + public void importEntriesMultilineAbstract() throws IOException, URISyntaxException { + Path file = Path.of(CffImporterTest.class.getResource("CffImporterTestMultilineAbstract.cff").toURI()); + List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); + BibEntry entry = bibEntries.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()); 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" +... + From c4bc13c7bb5759f8b409a056080cef5270c034eb Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Thu, 21 Mar 2024 13:36:48 +0100 Subject: [PATCH 32/38] feat: finished CFFExporter logic and crafted working round-trip test (#10995) --- .../jabref/logic/exporter/CffExporter.java | 188 ++++++++++-------- .../importer/fileformat/CffImporter.java | 2 +- .../logic/exporter/CffExporterTest.java | 131 ++++++------ .../importer/fileformat/CffImporterTest.java | 103 ++++------ 4 files changed, 212 insertions(+), 212 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index a97e7470cc0..de5adff98bb 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -93,125 +93,157 @@ public void export(BibDatabaseContext databaseContext, Path file, List return; } - DumperOptions options = new DumperOptions(); + // Make a copy of the list to avoid modifying the original list + entries = new ArrayList<>(entries); - // Set line width to infinity to avoid line wrapping + // Set up YAML options + DumperOptions options = new DumperOptions(); options.setWidth(Integer.MAX_VALUE); - - // Set collections to be written in block rather than inline options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setPrettyFlow(true); - - // Set indent for sequences to two spaces options.setIndentWithIndicator(true); options.setIndicatorIndent(2); Yaml yaml = new Yaml(options); - Map originalData = new LinkedHashMap<>(); - Map referencesData = new LinkedHashMap<>(); - Map data = originalData; - data.put("cff-version", "1.2.0"); - + // Check number of `software` or `dataset` entries + int counter = 0; + boolean dummy = false; + BibEntry main = null; for (BibEntry entry : entries) { - Map entryMap = new HashMap<>(entry.getFieldMap()); + if (entry.getType() == StandardEntryType.Software || entry.getType() == StandardEntryType.Dataset) { + main = entry; + counter++; + } + } + if (counter == 1) { + entries.remove(main); + } else { + main = new BibEntry(StandardEntryType.Software); + dummy = true; + } - // Mandatory message field - String message = entryMap.getOrDefault(StandardField.COMMENT, - "If you use this software, please cite it using the metadata from this file."); - data.put("message", message); - entryMap.remove(StandardField.COMMENT); + // Main entry + Map data = parseEntry(main, true, dummy); - // Mandatory title field - String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); - data.put("title", title); - entryMap.remove(StandardField.TITLE); - - // Mandatory authors field - List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) - .getAuthors(); - List> authorsList = parseAuthors(data, authors); - entryMap.remove(StandardField.AUTHOR); - - // Type; - EntryType entryType = entry.getType(); - switch (entryType) { - case StandardEntryType.Software, StandardEntryType.Dataset -> - data.put("type", entryType.getName()); - default -> { - if (TYPES_MAP.containsKey(entryType)) { - data.put("references", referencesData); - data = referencesData; - data.put("type", TYPES_MAP.get(entryType)); - data.put("authors", authorsList.isEmpty() ? - "No author specified." : authorsList); - data.put("title", title); - } - } + // Preferred citation + if (main.hasField(StandardField.CITES)) { + String citeKey = main.getField(StandardField.CITES).orElse("").split(",")[0]; + List citedEntries = databaseContext.getDatabase().getEntriesByCitationKey(citeKey); + entries.removeAll(citedEntries); + if (!citedEntries.isEmpty()) { + BibEntry citedEntry = citedEntries.getFirst(); + data.put("preferred-citation", parseEntry(citedEntry, false, false)); } + } - // Keywords - String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); - if (keywords != null) { - data.put("keywords", keywords.split(",\\s*")); + // References + List> related = new ArrayList<>(); + if (main.hasField(StandardField.RELATED)) { + String[] citeKeys = main.getField(StandardField.RELATED).orElse("").split(","); + List relatedEntries = new ArrayList<>(); + Arrays.stream(citeKeys).forEach(citeKey -> + relatedEntries.addAll(databaseContext.getDatabase().getEntriesByCitationKey(citeKey))); + entries.removeAll(relatedEntries); + if (!relatedEntries.isEmpty()) { + relatedEntries.forEach(entry -> related.add(parseEntry(entry, false, false))); } - entryMap.remove(StandardField.KEYWORDS); + } - // Date - String date = entryMap.getOrDefault(StandardField.DATE, null); - if (date != null) { - parseDate(data, date); - } - entryMap.remove(StandardField.DATE); - - // Fields - for (Field field : entryMap.keySet()) { - if (FIELDS_MAP.containsKey(field)) { - data.put(FIELDS_MAP.get(field), entryMap.get(field)); - } else if (field instanceof UnknownField) { - // Check that field is accepted by CFF format specification - if (UNMAPPED_FIELDS.contains(field.getName())) { - data.put(field.getName(), entryMap.get(field)); - } - } - } + // Add remaining entries as references + for (BibEntry entry : entries) { + related.add(parseEntry(entry, false, false)); + } + if (!related.isEmpty()) { + data.put("references", related); } + // Write to file try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { - yaml.dump(originalData, writer); + yaml.dump(data, writer); } catch ( IOException ex) { throw new SaveException(ex); } } - private List> parseAuthors(Map data, List authors) { + private Map parseEntry(BibEntry entry, boolean main, boolean dummy) { + Map data = new LinkedHashMap<>(); + Map entryMap = new HashMap<>(entry.getFieldMap()); + + if (main) { + // Mandatory CFF version field + data.put("cff-version", "1.2.0"); + + // Mandatory message field + String message = entryMap.getOrDefault(StandardField.COMMENT, + "If you use this software, please cite it using the metadata from this file."); + data.put("message", message); + entryMap.remove(StandardField.COMMENT); + } + + // Mandatory title field + String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); + data.put("title", title); + entryMap.remove(StandardField.TITLE); + + // Mandatory authors field + List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) + .getAuthors(); + parseAuthors(data, authors); + entryMap.remove(StandardField.AUTHOR); + + // Type + if (!dummy) { + data.put("type", TYPES_MAP.getOrDefault(entry.getType(), "misc")); + } + + // Keywords + String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); + if (keywords != null) { + data.put("keywords", keywords.split(",\\s*")); + } + entryMap.remove(StandardField.KEYWORDS); + + // Date + String date = entryMap.getOrDefault(StandardField.DATE, null); + if (date != null) { + parseDate(data, date); + } + entryMap.remove(StandardField.DATE); + + // Fields + for (Field field : entryMap.keySet()) { + if (FIELDS_MAP.containsKey(field)) { + data.put(FIELDS_MAP.get(field), entryMap.get(field)); + } else if (field instanceof UnknownField) { + // Check that field is accepted by CFF format specification + if (UNMAPPED_FIELDS.contains(field.getName())) { + data.put(field.getName(), entryMap.get(field)); + } + } + } + return data; + } + + private void parseAuthors(Map data, List authors) { List> authorsList = new ArrayList<>(); - // Copy the original list to avoid using YAML anchors and aliases; - List> authorsListCopy = new ArrayList<>(); authors.forEach(author -> { Map authorMap = new LinkedHashMap<>(); - Map authorMapCopy = new LinkedHashMap<>(); if (author.getFamilyName().isPresent()) { authorMap.put("family-names", author.getFamilyName().get()); - authorMapCopy.put("family-names", author.getFamilyName().get()); } if (author.getGivenName().isPresent()) { authorMap.put("given-names", author.getGivenName().get()); - authorMapCopy.put("given-names", author.getGivenName().get()); } if (author.getNamePrefix().isPresent()) { authorMap.put("name-particle", author.getNamePrefix().get()); - authorMapCopy.put("name-particle", author.getNamePrefix().get()); } if (author.getNameSuffix().isPresent()) { authorMap.put("name-suffix", author.getNameSuffix().get()); - authorMapCopy.put("name-suffix", author.getNameSuffix().get()); } authorsList.add(authorMap); - authorsListCopy.add(authorMapCopy); }); - data.put("authors", authorsList.isEmpty() ? "No author specified." : authorsList); - return authorsListCopy; + data.put("authors", authorsList.isEmpty() ? List.of(Map.of("name", "/")) : authorsList); } private void parseDate(Map data, String date) { 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 3162de41e92..7a180d79c38 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -253,7 +253,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { gen.generateAndSetKey(ref); String citeKey = ref.getCitationKey().orElse(""); String related = entry.getField(StandardField.RELATED).orElse(""); - entry.setField(StandardField.RELATED, related.isEmpty() ? citeKey : related + ", " + citeKey); + entry.setField(StandardField.RELATED, related.isEmpty() ? citeKey : related + "," + citeKey); }); } return res; diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java index d627a2e8e0b..f0daa7e0bd4 100644 --- a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -13,6 +13,7 @@ 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; @@ -61,17 +62,16 @@ public final void exportsCorrectContent(@TempDir Path tempDir) throws Exception 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", + "title: No title specified.", "authors:", - " - family-names: Author", - " given-names: Test", + " - name: /", "references:", - " type: article", - " authors:", - " - family-names: Author", - " given-names: Test", - " title: Test Title", - " url: http://example.com"); + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: article", + " url: http://example.com"); assertEquals(expected, Files.readAllLines(file)); } @@ -91,17 +91,16 @@ public final void usesCorrectType(@TempDir Path tempDir) throws Exception { 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", + "title: No title specified.", "authors:", - " - family-names: Author", - " given-names: Test", + " - name: /", "references:", - " type: conference-paper", - " authors:", - " - family-names: Author", - " given-names: Test", - " title: Test Title", - " doi: random_doi_value"); + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: conference-paper", + " doi: random_doi_value"); assertEquals(expected, Files.readAllLines(file)); } @@ -118,8 +117,13 @@ public final void usesCorrectDefaultValues(@TempDir Path tempDir) throws Excepti "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: No author specified." - ); + "authors:", + " - name: /", + "references:", + " - title: No title specified.", + " authors:", + " - name: /", + " type: misc"); assertEquals(expected, Files.readAllLines(file)); } @@ -189,18 +193,17 @@ public final void exportsArticleDateCorrectly(@TempDir Path tempDir) throws Exce 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", + "title: No title specified.", "authors:", - " - family-names: Author", - " given-names: Test", + " - name: /", "references:", - " type: article", - " authors:", - " - family-names: Author", - " given-names: Test", - " title: Test Title", - " month: 11", - " year: 2003"); + " - title: Test Title", + " authors:", + " - family-names: Author", + " given-names: Test", + " type: article", + " month: 11", + " year: 2003"); assertEquals(expected, Files.readAllLines(file)); } @@ -220,41 +223,39 @@ public final void passesModifiedCharset(@TempDir Path tempDir) throws Exception 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: 細雪", + "title: No title specified.", "authors:", - " - family-names: 潤一郎", - " given-names: 谷崎", + " - name: /", "references:", - " type: article", - " authors:", - " - family-names: 潤一郎", - " given-names: 谷崎", - " title: 細雪", - " url: http://example.com"); + " - 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 - CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class, Answers.RETURNS_SMART_NULLS); - when(citationKeyPatternPreferences.getKeyPattern()).thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); CffImporter importer = new CffImporter(citationKeyPatternPreferences); Path file = Path.of(CffImporterTest.class.getResource("CITATION.cff").toURI()); - List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); - BibEntry softwareEntry = bibEntries.getFirst(); - BibEntry articleEntry = bibEntries.getLast(); + BibDatabase db = importer.importDatabase(file).getDatabase(); + BibDatabaseContext dbc = new BibDatabaseContext(db); - // Then, export them separately and check they have all required fields - Path softwareFile = tempDir.resolve("CITATION_SOFTWARE.cff"); - Path articleFile = tempDir.resolve("CITATION_ARTICLE.cff"); - Files.createFile(softwareFile); - Files.createFile(articleFile); - - cffExporter.export(databaseContext, softwareFile, Collections.singletonList(softwareEntry)); - cffExporter.export(databaseContext, articleFile, Collections.singletonList(articleEntry)); + // 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", @@ -285,21 +286,9 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { "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"); - - Set expectedArticle = Set.of( - "cff-version: 1.2.0", - "message: If you use this software, please cite it using the metadata from this file.", - "title: 'JabRef: BibTeX-based literature management software'", - "authors:", - " - family-names: Kopp", - " given-names: Oliver", - " - family-names: Snethlage", - " given-names: Carl Christian", - " - family-names: Schwentker", - " given-names: Christoph", - "references:", - " type: article", + "url: https://www.jabref.org", + "preferred-citation:", + " title: 'JabRef: BibTeX-based literature management software'", " authors:", " - family-names: Kopp", " given-names: Oliver", @@ -307,7 +296,7 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " given-names: Carl Christian", " - family-names: Schwentker", " given-names: Christoph", - " title: 'JabRef: BibTeX-based literature management software'", + " type: article", " month: '11'", " issue: '138'", " volume: '44'", @@ -319,13 +308,9 @@ public final void roundTripTest(@TempDir Path tempDir) throws Exception { " end: '447'"); // Tests equality of sets since last lines order is random and relies on entries internal order - try (Stream st = Files.lines(softwareFile)) { + try (Stream st = Files.lines(out)) { assertEquals(expectedSoftware, st.collect(Collectors.toSet())); } - - try (Stream st = Files.lines(articleFile)) { - assertEquals(expectedArticle, st.collect(Collectors.toSet())); - } } } 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 31ff9439a23..b9f0dedbcd4 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/CffImporterTest.java @@ -31,8 +31,12 @@ public class CffImporterTest { @BeforeEach public void setUp() { - CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class, Answers.RETURNS_SMART_NULLS); - when(citationKeyPatternPreferences.getKeyPattern()).thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); + CitationKeyPatternPreferences citationKeyPatternPreferences = mock( + CitationKeyPatternPreferences.class, + Answers.RETURNS_SMART_NULLS + ); + when(citationKeyPatternPreferences.getKeyPattern()) + .thenReturn(GlobalCitationKeyPattern.fromPattern("[auth][year]")); importer = new CffImporter(citationKeyPatternPreferences); } @@ -66,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)); @@ -78,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); } @@ -89,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); } @@ -100,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); } @@ -111,52 +109,40 @@ 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()); - List bibEntries = importer.importDatabase(file).getDatabase().getEntries(); - BibEntry entry = bibEntries.getFirst(); - + BibEntry entry = importer.importDatabase(file).getDatabase().getEntries().getFirst(); BibEntry expected = getPopulatedEntry().withField(StandardField.ABSTRACT, """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. @@ -172,7 +158,6 @@ public void importEntriesMultilineAbstract() throws IOException, URISyntaxExcept Aenean urna purus, lobortis a condimentum et, varius ut augue. Praesent ac lectus id mi posuere elementum. """); - assertEquals(entry, expected); } @@ -185,13 +170,13 @@ public void importEntriesPreferredCitation() throws IOException, URISyntaxExcept BibEntry preferredEntry = bibEntries.getLast(); String citeKey = preferredEntry.getCitationKey().orElse(""); - BibEntry expectedMain = getPopulatedEntry(); - expectedMain.setField(StandardField.CITES, citeKey); + BibEntry expectedMain = getPopulatedEntry().withField(StandardField.CITES, citeKey); - BibEntry expectedPreferred = new BibEntry(StandardEntryType.InProceedings).withCitationKey(citeKey); - expectedPreferred.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); - expectedPreferred.setField(StandardField.DOI, "10.0001/TEST"); - expectedPreferred.setField(StandardField.URL, "www.github.com"); + 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); @@ -207,19 +192,20 @@ public void importEntriesReferences() throws IOException, URISyntaxException { String citeKey1 = referenceEntry1.getCitationKey().orElse(""); String citeKey2 = referenceEntry2.getCitationKey().orElse(""); - BibEntry expectedMain = getPopulatedEntry(); - expectedMain.setField(StandardField.RELATED, citeKey1 + ", " + citeKey2); + BibEntry expectedMain = getPopulatedEntry().withField(StandardField.RELATED, citeKey1 + "," + citeKey2); - BibEntry expectedReference1 = new BibEntry(StandardEntryType.InProceedings).withCitationKey(citeKey1); - expectedReference1.setField(StandardField.AUTHOR, "Jonathan von Duke and Jim Kingston, Jr."); - expectedReference1.setField(StandardField.YEAR, "2007"); - expectedReference1.setField(StandardField.DOI, "10.0001/TEST"); - expectedReference1.setField(StandardField.URL, "www.example.com"); + 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); - expectedReference2.setField(StandardField.AUTHOR, "Arthur Clark, Jr. and Luca von Diamond"); - expectedReference2.setField(StandardField.DOI, "10.0002/TEST"); - expectedReference2.setField(StandardField.URL, "www.facebook.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); @@ -227,19 +213,16 @@ public void importEntriesReferences() throws IOException, URISyntaxException { } 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"); } } From 2450c80025c44066fd725d23ceb573473511127d Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Thu, 21 Mar 2024 13:53:15 +0100 Subject: [PATCH 33/38] fix: fixed typos in MADR decision doc for CFF export and refactore ImportFormatReader signature (#10995) --- .../0029-cff-export-multiple-entries.md | 20 +++++++++---------- .../org/jabref/cli/ArgumentProcessor.java | 5 +++-- src/main/java/org/jabref/cli/JabRefCLI.java | 5 +++-- .../gui/externalfiles/ImportHandler.java | 5 +++-- .../jabref/gui/importer/ImportCommand.java | 10 ++++++---- .../logic/importer/ImportFormatReader.java | 3 +-- .../ImportFormatReaderIntegrationTest.java | 4 ++-- .../ImportFormatReaderParameterlessTest.java | 2 +- 8 files changed, 28 insertions(+), 26 deletions(-) diff --git a/docs/decisions/0029-cff-export-multiple-entries.md b/docs/decisions/0029-cff-export-multiple-entries.md index 8814e28a4d8..179ceab745d 100644 --- a/docs/decisions/0029-cff-export-multiple-entries.md +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -9,9 +9,8 @@ parent: Decision Records ## 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. +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 @@ -19,14 +18,15 @@ The need for an [exporter](https://github.com/JabRef/jabref/issues/10661) to [CF ## Considered Options -* When exporting : - * Export non-`software` entries with dummy topmost `sofware` element as `preferred-citation` - * Export non-`software` entries with dummy topmost `sofware` element as `references` +* 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 : +* 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 @@ -38,20 +38,18 @@ 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 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. +* 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 +* 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/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index 0e43846ed8f..ef203b4f3aa 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -166,8 +166,9 @@ private Optional importFile(Path file, String importFormat) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor, - preferencesService.getCitationKeyPatternPreferences()); + 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 96eba9a143e..3d613468af5 100644 --- a/src/main/java/org/jabref/cli/JabRefCLI.java +++ b/src/main/java/org/jabref/cli/JabRefCLI.java @@ -317,8 +317,9 @@ public static void printUsage(PreferencesService preferencesService) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - new DummyFileUpdateMonitor(), - preferencesService.getCitationKeyPatternPreferences()); + 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 131d93c809e..ef7ce0c5860 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -372,8 +372,9 @@ private List tryImportFormats(String data) { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor, - preferencesService.getCitationKeyPatternPreferences()); + 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 8654fbcf959..8fc8c072820 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCommand.java +++ b/src/main/java/org/jabref/gui/importer/ImportCommand.java @@ -79,8 +79,9 @@ public void execute() { ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor, - preferencesService.getCitationKeyPatternPreferences()); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); SortedSet importers = importFormatReader.getImportFormats(); FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() @@ -135,8 +136,9 @@ private ParserResult doImport(List files, Importer importFormat) throws IO ImportFormatReader importFormatReader = new ImportFormatReader( preferencesService.getImporterPreferences(), preferencesService.getImportFormatPreferences(), - fileUpdateMonitor, - preferencesService.getCitationKeyPatternPreferences()); + preferencesService.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); for (Path filename : files) { try { if (importer.isEmpty()) { diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index 7173aadd4e0..df54b8cf52f 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -55,8 +55,7 @@ public class ImportFormatReader { public ImportFormatReader(ImporterPreferences importerPreferences, ImportFormatPreferences importFormatPreferences, - FileUpdateMonitor fileUpdateMonitor, - CitationKeyPatternPreferences citationKeyPatternPreferences) { + CitationKeyPatternPreferences citationKeyPatternPreferences, FileUpdateMonitor fileUpdateMonitor) { this.importerPreferences = importerPreferences; this.importFormatPreferences = importFormatPreferences; this.fileUpdateMonitor = fileUpdateMonitor; diff --git a/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java b/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java index 64045daac54..7bbef2de9b4 100644 --- a/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java +++ b/src/test/java/org/jabref/logic/importer/ImportFormatReaderIntegrationTest.java @@ -31,8 +31,8 @@ void setUp() { reader = new ImportFormatReader( importerPreferences, mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), - new DummyFileUpdateMonitor(), - mock(CitationKeyPatternPreferences.class)); + 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 4858f7c5b11..39f3fcc2f2e 100644 --- a/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java +++ b/src/test/java/org/jabref/logic/importer/ImportFormatReaderParameterlessTest.java @@ -26,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, mock(CitationKeyPatternPreferences.class)); + reader = new ImportFormatReader(importerPreferences, importFormatPreferences, mock(CitationKeyPatternPreferences.class), fileMonitor); } @Test From 8d72c5f1897ec2972d5bf63448b2caec7d042bc2 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 21 Mar 2024 22:59:52 +0100 Subject: [PATCH 34/38] Some code beautification --- .../jabref/logic/exporter/CffExporter.java | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index de5adff98bb..90b6206371c 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -33,13 +33,15 @@ import org.yaml.snakeyaml.Yaml; public class CffExporter extends Exporter { - public static final List UNMAPPED_FIELDS = Arrays.asList( + // 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"), @@ -89,7 +91,8 @@ public void export(BibDatabaseContext databaseContext, Path file, List Objects.requireNonNull(file); Objects.requireNonNull(entries); - if (entries.isEmpty()) { // Do not export if no entries to export -- avoids exports with only template text + // Do not export if no entries to export -- avoids exports with only template text + if (entries.isEmpty()) { return; } @@ -105,25 +108,26 @@ public void export(BibDatabaseContext databaseContext, Path file, List options.setIndicatorIndent(2); Yaml yaml = new Yaml(options); - // Check number of `software` or `dataset` entries - int counter = 0; - boolean dummy = false; BibEntry main = null; + boolean mainIsDummy = false; + int countofSoftwareAndDataSetEntries = 0; for (BibEntry entry : entries) { if (entry.getType() == StandardEntryType.Software || entry.getType() == StandardEntryType.Dataset) { main = entry; - counter++; + countofSoftwareAndDataSetEntries++; } } - if (counter == 1) { + if (countofSoftwareAndDataSetEntries == 1) { + // If there is only one software or dataset entry, use it as the main entry entries.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); - dummy = true; + mainIsDummy = true; } - // Main entry - Map data = parseEntry(main, true, dummy); + // Transform main entry to CFF format + Map cffData = transformEntry(main, true, mainIsDummy); // Preferred citation if (main.hasField(StandardField.CITES)) { @@ -132,7 +136,7 @@ public void export(BibDatabaseContext databaseContext, Path file, List entries.removeAll(citedEntries); if (!citedEntries.isEmpty()) { BibEntry citedEntry = citedEntries.getFirst(); - data.put("preferred-citation", parseEntry(citedEntry, false, false)); + cffData.put("preferred-citation", transformEntry(citedEntry, false, false)); } } @@ -145,84 +149,82 @@ public void export(BibDatabaseContext databaseContext, Path file, List relatedEntries.addAll(databaseContext.getDatabase().getEntriesByCitationKey(citeKey))); entries.removeAll(relatedEntries); if (!relatedEntries.isEmpty()) { - relatedEntries.forEach(entry -> related.add(parseEntry(entry, false, false))); + relatedEntries.forEach(entry -> related.add(transformEntry(entry, false, false))); } } // Add remaining entries as references for (BibEntry entry : entries) { - related.add(parseEntry(entry, false, false)); + related.add(transformEntry(entry, false, false)); } if (!related.isEmpty()) { - data.put("references", related); + cffData.put("references", related); } - // Write to file try (FileWriter writer = new FileWriter(file.toFile(), StandardCharsets.UTF_8)) { - yaml.dump(data, writer); - } catch ( - IOException ex) { + yaml.dump(cffData, writer); + } catch (IOException ex) { throw new SaveException(ex); } } - private Map parseEntry(BibEntry entry, boolean main, boolean dummy) { - Map data = new LinkedHashMap<>(); - Map entryMap = new HashMap<>(entry.getFieldMap()); + 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 - data.put("cff-version", "1.2.0"); + cffData.put("cff-version", "1.2.0"); // Mandatory message field - String message = entryMap.getOrDefault(StandardField.COMMENT, + String message = fields.getOrDefault(StandardField.COMMENT, "If you use this software, please cite it using the metadata from this file."); - data.put("message", message); - entryMap.remove(StandardField.COMMENT); + cffData.put("message", message); + fields.remove(StandardField.COMMENT); } // Mandatory title field - String title = entryMap.getOrDefault(StandardField.TITLE, "No title specified."); - data.put("title", title); - entryMap.remove(StandardField.TITLE); + String title = fields.getOrDefault(StandardField.TITLE, "No title specified."); + cffData.put("title", title); + fields.remove(StandardField.TITLE); // Mandatory authors field - List authors = AuthorList.parse(entryMap.getOrDefault(StandardField.AUTHOR, "")) + List authors = AuthorList.parse(fields.getOrDefault(StandardField.AUTHOR, "")) .getAuthors(); - parseAuthors(data, authors); - entryMap.remove(StandardField.AUTHOR); + parseAuthors(cffData, authors); + fields.remove(StandardField.AUTHOR); // Type if (!dummy) { - data.put("type", TYPES_MAP.getOrDefault(entry.getType(), "misc")); + cffData.put("type", TYPES_MAP.getOrDefault(entry.getType(), "misc")); } // Keywords - String keywords = entryMap.getOrDefault(StandardField.KEYWORDS, null); + String keywords = fields.getOrDefault(StandardField.KEYWORDS, null); if (keywords != null) { - data.put("keywords", keywords.split(",\\s*")); + cffData.put("keywords", keywords.split(",\\s*")); } - entryMap.remove(StandardField.KEYWORDS); + fields.remove(StandardField.KEYWORDS); // Date - String date = entryMap.getOrDefault(StandardField.DATE, null); + String date = fields.getOrDefault(StandardField.DATE, null); if (date != null) { - parseDate(data, date); + parseDate(cffData, date); } - entryMap.remove(StandardField.DATE); + fields.remove(StandardField.DATE); - // Fields - for (Field field : entryMap.keySet()) { + // Remaining fields not handled above + for (Field field : fields.keySet()) { if (FIELDS_MAP.containsKey(field)) { - data.put(FIELDS_MAP.get(field), entryMap.get(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())) { - data.put(field.getName(), entryMap.get(field)); + cffData.put(field.getName(), fields.get(field)); } } } - return data; + return cffData; } private void parseAuthors(Map data, List authors) { From bf9ff8bfb382a07f4307b48646142cb7748f4356 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 21 Mar 2024 23:08:41 +0100 Subject: [PATCH 35/38] Use existing method getEntryLinkList --- .../jabref/logic/exporter/CffExporter.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 90b6206371c..7e69a1fd70c 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -9,12 +9,12 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; -import java.util.Arrays; 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.layout.format.DateFormatter; import org.jabref.logic.util.StandardFileType; @@ -97,7 +97,7 @@ public void export(BibDatabaseContext databaseContext, Path file, List } // Make a copy of the list to avoid modifying the original list - entries = new ArrayList<>(entries); + final List entriesToTransform = new ArrayList<>(entries); // Set up YAML options DumperOptions options = new DumperOptions(); @@ -110,16 +110,16 @@ public void export(BibDatabaseContext databaseContext, Path file, List BibEntry main = null; boolean mainIsDummy = false; - int countofSoftwareAndDataSetEntries = 0; - for (BibEntry entry : entries) { + int countOfSoftwareAndDataSetEntries = 0; + for (BibEntry entry : entriesToTransform) { if (entry.getType() == StandardEntryType.Software || entry.getType() == StandardEntryType.Dataset) { main = entry; - countofSoftwareAndDataSetEntries++; + countOfSoftwareAndDataSetEntries++; } } - if (countofSoftwareAndDataSetEntries == 1) { + if (countOfSoftwareAndDataSetEntries == 1) { // If there is only one software or dataset entry, use it as the main entry - entries.remove(main); + 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); @@ -133,7 +133,7 @@ public void export(BibDatabaseContext databaseContext, Path file, List if (main.hasField(StandardField.CITES)) { String citeKey = main.getField(StandardField.CITES).orElse("").split(",")[0]; List citedEntries = databaseContext.getDatabase().getEntriesByCitationKey(citeKey); - entries.removeAll(citedEntries); + entriesToTransform.removeAll(citedEntries); if (!citedEntries.isEmpty()) { BibEntry citedEntry = citedEntries.getFirst(); cffData.put("preferred-citation", transformEntry(citedEntry, false, false)); @@ -143,18 +143,19 @@ public void export(BibDatabaseContext databaseContext, Path file, List // References List> related = new ArrayList<>(); if (main.hasField(StandardField.RELATED)) { - String[] citeKeys = main.getField(StandardField.RELATED).orElse("").split(","); - List relatedEntries = new ArrayList<>(); - Arrays.stream(citeKeys).forEach(citeKey -> - relatedEntries.addAll(databaseContext.getDatabase().getEntriesByCitationKey(citeKey))); - entries.removeAll(relatedEntries); - if (!relatedEntries.isEmpty()) { - relatedEntries.forEach(entry -> related.add(transformEntry(entry, false, false))); - } + 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 : entries) { + for (BibEntry entry : entriesToTransform) { related.add(transformEntry(entry, false, false)); } if (!related.isEmpty()) { From 447632b77d2c632fb1a633f1aec45494b24b5e2b Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 21 Mar 2024 23:22:42 +0100 Subject: [PATCH 36/38] Use getEntryLinkList --- .../logic/integrity/EntryLinkChecker.java | 21 ++++++------------- .../org/jabref/model/entry/EntryLinkList.java | 4 ++-- src/main/resources/l10n/JabRef_en.properties | 2 +- 3 files changed, 9 insertions(+), 18 deletions(-) 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/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. From c43d14ab59ae387f37a8aa94ca0a1eae29eea56a Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 21 Mar 2024 23:26:19 +0100 Subject: [PATCH 37/38] Use JabRef's Date class for parsing --- .../jabref/logic/exporter/CffExporter.java | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/jabref/logic/exporter/CffExporter.java b/src/main/java/org/jabref/logic/exporter/CffExporter.java index 7e69a1fd70c..210453e4e21 100644 --- a/src/main/java/org/jabref/logic/exporter/CffExporter.java +++ b/src/main/java/org/jabref/logic/exporter/CffExporter.java @@ -4,10 +4,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.time.Year; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -16,12 +12,12 @@ import java.util.Objects; import java.util.Optional; -import org.jabref.logic.layout.format.DateFormatter; 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; @@ -250,33 +246,18 @@ private void parseAuthors(Map data, List authors) { } private void parseDate(Map data, String date) { - String formatString; - try { - DateFormatter dateFormatter = new DateFormatter(); - data.put("date-released", dateFormatter.format(date)); - } catch ( - DateTimeParseException e) { - try { - formatString = "yyyy-MM"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - YearMonth yearMonth = YearMonth.parse(date, formatter); - int month = yearMonth.getMonth().getValue(); - int year = yearMonth.getYear(); - data.put("month", month); - data.put("year", year); - } catch ( - DateTimeParseException f) { - try { - formatString = "yyyy"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); - int year = Year.parse(date, formatter).getValue(); - data.put("year", year); - } catch ( - DateTimeParseException g) { - data.put("issue-date", 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)); } } From 60904dac79269da151006fd627943143459346c8 Mon Sep 17 00:00:00 2001 From: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:38:18 +0100 Subject: [PATCH 38/38] Fix indentation in new line --- .../java/org/jabref/logic/importer/ImportFormatReader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index df54b8cf52f..7b097816db7 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -55,7 +55,8 @@ public class ImportFormatReader { public ImportFormatReader(ImporterPreferences importerPreferences, ImportFormatPreferences importFormatPreferences, - CitationKeyPatternPreferences citationKeyPatternPreferences, FileUpdateMonitor fileUpdateMonitor) { + CitationKeyPatternPreferences citationKeyPatternPreferences, + FileUpdateMonitor fileUpdateMonitor) { this.importerPreferences = importerPreferences; this.importFormatPreferences = importFormatPreferences; this.fileUpdateMonitor = fileUpdateMonitor;