From d303aff3a8cded12cf943e391254ddb42a6f52a5 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] 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 96db4a75227..cdd097ee56d 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()); + } }