diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index cd4e96ce523..e15daf1997f 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -41,6 +41,7 @@ import org.jabref.logic.search.SearchQuery; import org.jabref.logic.shared.prefs.SharedDatabasePreferences; import org.jabref.logic.util.OS; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.Defaults; import org.jabref.model.EntryTypes; import org.jabref.model.database.BibDatabase; @@ -471,7 +472,8 @@ private void importPreferences() { LayoutFormatterPreferences layoutPreferences = Globals.prefs .getLayoutFormatterPreferences(Globals.journalAbbreviationLoader); SavePreferences savePreferences = SavePreferences.loadForExportFromPreferences(Globals.prefs); - Globals.exportFactory = ExporterFactory.create(customExporters, layoutPreferences, savePreferences); + XmpPreferences xmpPreferences = Globals.prefs.getXMPPreferences(); + Globals.exportFactory = ExporterFactory.create(customExporters, layoutPreferences, savePreferences, xmpPreferences); } catch (JabRefException ex) { LOGGER.error("Cannot import preferences", ex); } diff --git a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java index 5eb9145f6c5..efd88ae7ea4 100644 --- a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java +++ b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java @@ -11,6 +11,7 @@ import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.layout.LayoutFormatterPreferences; import org.jabref.logic.util.FileType; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.preferences.JabRefPreferences; public class ExporterFactory { @@ -29,7 +30,7 @@ private ExporterFactory(List exporters) { } public static ExporterFactory create(Map customFormats, - LayoutFormatterPreferences layoutPreferences, SavePreferences savePreferences) { + LayoutFormatterPreferences layoutPreferences, SavePreferences savePreferences, XmpPreferences xmpPreferences) { List exporters = new ArrayList<>(); @@ -54,6 +55,7 @@ public static ExporterFactory create(Map customFormats exporters.add(new OpenDocumentSpreadsheetCreator()); exporters.add(new MSBibExporter()); exporters.add(new ModsExporter()); + exporters.add(new XmpExporter(xmpPreferences)); // Now add custom export formats exporters.addAll(customFormats.values()); @@ -65,7 +67,8 @@ public static ExporterFactory create(JabRefPreferences preferences, JournalAbbre Map customFormats = preferences.customExports.getCustomExportFormats(preferences, abbreviationLoader); LayoutFormatterPreferences layoutPreferences = preferences.getLayoutFormatterPreferences(abbreviationLoader); SavePreferences savePreferences = SavePreferences.loadForExportFromPreferences(preferences); - return create(customFormats, layoutPreferences, savePreferences); + XmpPreferences xmpPreferences = preferences.getXMPPreferences(); + return create(customFormats, layoutPreferences, savePreferences, xmpPreferences); } /** diff --git a/src/main/java/org/jabref/logic/exporter/XmpExporter.java b/src/main/java/org/jabref/logic/exporter/XmpExporter.java new file mode 100644 index 00000000000..6a08536ec1c --- /dev/null +++ b/src/main/java/org/jabref/logic/exporter/XmpExporter.java @@ -0,0 +1,72 @@ +package org.jabref.logic.exporter; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.jabref.logic.util.FileType; +import org.jabref.logic.xmp.XmpPreferences; +import org.jabref.logic.xmp.XmpUtilWriter; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +/** + * A custom exporter to write bib entries to a .xmp file for further processing + * in other scenarios and applications. The xmp metadata are written in dublin + * core format. + */ +public class XmpExporter extends Exporter { + + private static final String XMP_SPLIT_PATTERN = "split"; + + private final XmpPreferences xmpPreferences; + + public XmpExporter(XmpPreferences xmpPreferences) { + super("xmp", FileType.PLAIN_XMP.getDescription(), FileType.PLAIN_XMP); + this.xmpPreferences = xmpPreferences; + } + + @Override + public void export(BibDatabaseContext databaseContext, Path file, Charset encoding, List entries) throws Exception { + Objects.requireNonNull(databaseContext); + Objects.requireNonNull(file); + Objects.requireNonNull(entries); + + if (entries.isEmpty()) { + return; + } + + // This is a distinction between writing all entries from the supplied list to a single .xmp file, + // or write every entry to a separate file. + if (file.getFileName().toString().trim().equals(XMP_SPLIT_PATTERN)) { + + for (BibEntry entry : entries) { + // Avoid situations, where two cite keys are null + Path entryFile; + String suffix = entry.getId() + "_" + entry.getCiteKey() + ".xmp"; + if (file.getParent() == null) { + entryFile = Paths.get(suffix); + } else { + entryFile = Paths.get(file.getParent().toString() + "/" + suffix); + } + + this.writeBibToXmp(entryFile, Arrays.asList(entry), encoding); + } + } else { + this.writeBibToXmp(file, entries, encoding); + } + } + + private void writeBibToXmp(Path file, List entries, Charset encoding) throws IOException { + try (BufferedWriter writer = Files.newBufferedWriter(file, encoding)) { + writer.write(XmpUtilWriter.generateXmpString(entries, this.xmpPreferences)); + writer.flush(); + } + } +} diff --git a/src/main/java/org/jabref/logic/util/FileType.java b/src/main/java/org/jabref/logic/util/FileType.java index cb1de601dc0..400a88540ed 100644 --- a/src/main/java/org/jabref/logic/util/FileType.java +++ b/src/main/java/org/jabref/logic/util/FileType.java @@ -50,6 +50,7 @@ public enum FileType { SILVER_PLATTER(Localization.lang("%0 file", "SilverPlatter"), "dat", "txt"), SIMPLE_HTML(Localization.lang("%0 file", Localization.lang("Simple HTML")), "html"), XMP(Localization.lang("XMP-annotated PDF"), "pdf"), + PLAIN_XMP(Localization.lang("%0 file", "XMP"), "xmp"), AUX(Localization.lang("%0 file", "AUX"), "aux"), JSTYLE(Localization.lang("Style file"), "jstyle"), diff --git a/src/main/java/org/jabref/logic/xmp/XmpUtilWriter.java b/src/main/java/org/jabref/logic/xmp/XmpUtilWriter.java index 45f10167009..9e14464f0b0 100644 --- a/src/main/java/org/jabref/logic/xmp/XmpUtilWriter.java +++ b/src/main/java/org/jabref/logic/xmp/XmpUtilWriter.java @@ -3,6 +3,8 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -82,15 +84,38 @@ public static void writeXmp(Path file, BibEntry entry, XmpUtilWriter.writeXmp(file, bibEntryList, database, xmpPreferences); } + /** + * Writes the information of the bib entry to the dublin core schema using + * a custom extractor. + * + * @param dcSchema Dublin core schema, which is filled with the bib entry. + * @param entry The entry, which is added to the dublin core metadata. + * @param database maybenull An optional database which the given bibtex entries belong to, which will be used to + * resolve strings. If the database is null the strings will not be resolved. + * @param xmpPreferences The user's xmp preferences. + */ private static void writeToDCSchema(DublinCoreSchema dcSchema, BibEntry entry, BibDatabase database, XmpPreferences xmpPreferences) { BibEntry resolvedEntry = XmpUtilWriter.getDefaultOrDatabaseEntry(entry, database); - DublinCoreExtractor dcExtractor = new DublinCoreExtractor(dcSchema, xmpPreferences, resolvedEntry); - dcExtractor.fillDublinCoreSchema(); + writeToDCSchema(dcSchema, resolvedEntry, xmpPreferences); } + /** + * Writes the information of the bib entry to the dublin core schema using + * a custom extractor. + * + * @param dcSchema Dublin core schema, which is filled with the bib entry. + * @param entry The entry, which is added to the dublin core metadata. + * @param xmpPreferences The user's xmp preferences. + */ + private static void writeToDCSchema(DublinCoreSchema dcSchema, BibEntry entry, + XmpPreferences xmpPreferences) { + + DublinCoreExtractor dcExtractor = new DublinCoreExtractor(dcSchema, xmpPreferences, entry); + dcExtractor.fillDublinCoreSchema(); + } /** * Try to write the given BibTexEntry as a DublinCore XMP Schema @@ -159,6 +184,38 @@ private static void writeDublinCore(PDDocument document, catalog.setMetadata(metadataStream); } + /** + * This method generates an xmp metadata string in dublin core format. + *
+ * + * @param entries A list of entries, which are added to the dublin core metadata. + * @param xmpPreferences The user's xmp preferences. + * + * @return If something goes wrong (e.g. an exception is thrown), the method returns an empty string, + * otherwise it returns the xmp metadata as a string in dublin core format. + */ + public static String generateXmpString(List entries, XmpPreferences xmpPreferences) { + XMPMetadata meta = XMPMetadata.createXMPMetadata(); + for (BibEntry entry : entries) { + DublinCoreSchema dcSchema = meta.createAndAddDublinCoreSchema(); + XmpUtilWriter.writeToDCSchema(dcSchema, entry, xmpPreferences); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + XmpSerializer serializer = new XmpSerializer(); + serializer.serialize(meta, os, true); + return os.toString(StandardCharsets.UTF_8.name()); + } catch (TransformerException e) { + LOGGER.warn("Tranformation into xmp not possible: " + e.getMessage(), e); + return ""; + } catch (UnsupportedEncodingException e) { + LOGGER.warn("Unsupported encoding to UTF-8 of bib entries in xmp metadata.", e); + return ""; + } catch (IOException e) { + LOGGER.warn("IO Exception thrown by closing the output stream.", e); + return ""; + } + } + /** * Try to write the given BibTexEntry in the Document Information (the * properties of the pdf). diff --git a/src/test/java/org/jabref/logic/exporter/CsvExportFormatTest.java b/src/test/java/org/jabref/logic/exporter/CsvExportFormatTest.java index c620578c826..ce7d1058d25 100644 --- a/src/test/java/org/jabref/logic/exporter/CsvExportFormatTest.java +++ b/src/test/java/org/jabref/logic/exporter/CsvExportFormatTest.java @@ -10,6 +10,7 @@ import java.util.Map; import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -36,7 +37,8 @@ public void setUp() { Map customFormats = new HashMap<>(); LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS); SavePreferences savePreferences = mock(SavePreferences.class); - ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences); + XmpPreferences xmpPreferences = mock(XmpPreferences.class); + ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences); exportFormat = exporterFactory.getExporterByName("oocsv").get(); diff --git a/src/test/java/org/jabref/logic/exporter/ExporterTest.java b/src/test/java/org/jabref/logic/exporter/ExporterTest.java index 66dfa4ccc00..7f9584dafe7 100644 --- a/src/test/java/org/jabref/logic/exporter/ExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/ExporterTest.java @@ -12,6 +12,7 @@ import java.util.Map; import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -57,7 +58,8 @@ public static Collection exportFormats() { Map customFormats = new HashMap<>(); LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class); SavePreferences savePreferences = mock(SavePreferences.class); - ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences); + XmpPreferences xmpPreferences = mock(XmpPreferences.class); + ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences); for (Exporter format : exporterFactory.getExporters()) { result.add(new Object[]{format, format.getDisplayName()}); diff --git a/src/test/java/org/jabref/logic/exporter/HtmlExportFormatTest.java b/src/test/java/org/jabref/logic/exporter/HtmlExportFormatTest.java index b3661c61a88..053f16f935a 100644 --- a/src/test/java/org/jabref/logic/exporter/HtmlExportFormatTest.java +++ b/src/test/java/org/jabref/logic/exporter/HtmlExportFormatTest.java @@ -10,6 +10,7 @@ import java.util.Map; import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -37,7 +38,8 @@ public void setUp() { Map customFormats = new HashMap<>(); LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS); SavePreferences savePreferences = mock(SavePreferences.class); - ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences); + XmpPreferences xmpPreferences = mock(XmpPreferences.class); + ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences); exportFormat = exporterFactory.getExporterByName("html").get(); diff --git a/src/test/java/org/jabref/logic/exporter/XmpExporterTest.java b/src/test/java/org/jabref/logic/exporter/XmpExporterTest.java new file mode 100644 index 00000000000..28532bed71d --- /dev/null +++ b/src/test/java/org/jabref/logic/exporter/XmpExporterTest.java @@ -0,0 +1,112 @@ +package org.jabref.logic.exporter; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.xmp.XmpPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Answers; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class XmpExporterTest { + + private Exporter exporter; + private BibDatabaseContext databaseContext; + private Charset encoding; + + @Rule public TemporaryFolder testFolder = new TemporaryFolder(); + + @Before + public void setUp() { + Map customFormats = new HashMap<>(); + LayoutFormatterPreferences layoutPreferences = mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS); + SavePreferences savePreferences = mock(SavePreferences.class); + XmpPreferences xmpPreferences = mock(XmpPreferences.class); + ExporterFactory exporterFactory = ExporterFactory.create(customFormats, layoutPreferences, savePreferences, xmpPreferences); + + exporter = exporterFactory.getExporterByName("xmp").get(); + + databaseContext = new BibDatabaseContext(); + encoding = StandardCharsets.UTF_8; + } + + @Test + public void exportSingleEntry() throws Exception { + + Path file = testFolder.newFile().toPath(); + + BibEntry entry = new BibEntry(); + entry.setField("author", "Alan Turing"); + + exporter.export(databaseContext, file, encoding, Arrays.asList(entry)); + + List lines = Files.readAllLines(file); + assertTrue(lines.size() == 21); + assertEquals("Alan Turing", lines.get(7).trim()); + } + + @Test + public void writeMutlipleEntriesInASingleFile() throws Exception { + + Path file = testFolder.newFile().toPath(); + + BibEntry entryTuring = new BibEntry(); + entryTuring.setField("author", "Alan Turing"); + + BibEntry entryArmbrust = new BibEntry(); + entryArmbrust.setField("author", "Michael Armbrust"); + entryArmbrust.setCiteKey("Armbrust2010"); + + exporter.export(databaseContext, file, encoding, Arrays.asList(entryTuring, entryArmbrust)); + + List lines = Files.readAllLines(file); + assertTrue(lines.size() == 39); + assertEquals("Alan Turing", lines.get(7).trim()); + assertEquals("Michael Armbrust", lines.get(20).trim()); + } + + @Test + public void writeMultipleEntriesInDifferentFiles() throws Exception { + + Path file = testFolder.newFile("split").toPath(); + + BibEntry entryTuring = new BibEntry(); + entryTuring.setField("author", "Alan Turing"); + + BibEntry entryArmbrust = new BibEntry(); + entryArmbrust.setField("author", "Michael Armbrust"); + entryArmbrust.setCiteKey("Armbrust2010"); + + exporter.export(databaseContext, file, encoding, Arrays.asList(entryTuring, entryArmbrust)); + + List lines = Files.readAllLines(file); + assertTrue(lines.size() == 0); + + Path fileTuring = Paths.get(file.getParent().toString() + "/" + entryTuring.getId() + "_null.xmp"); + List linesTuring = Files.readAllLines(fileTuring); + assertTrue(linesTuring.size() == 21); + assertEquals("Alan Turing", linesTuring.get(7).trim()); + + Path fileArmbrust = Paths.get(file.getParent().toString() + "/" + entryArmbrust.getId() + "_Armbrust2010.xmp"); + List linesArmbrust = Files.readAllLines(fileArmbrust); + assertTrue(linesArmbrust.size() == 26); + assertEquals("Michael Armbrust", linesArmbrust.get(7).trim()); + } +}