diff --git a/src/main/java/org/jabref/logic/shared/DBMSProcessor.java b/src/main/java/org/jabref/logic/shared/DBMSProcessor.java index e361ba20875..9192c915d21 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSProcessor.java +++ b/src/main/java/org/jabref/logic/shared/DBMSProcessor.java @@ -473,7 +473,10 @@ public List getSharedEntries(List sharedIDs) { lastId = selectEntryResultSet.getInt("SHARED_ID"); } - bibEntry.setField(FieldFactory.parseField(selectEntryResultSet.getString("NAME")), Optional.ofNullable(selectEntryResultSet.getString("VALUE")), EntryEventSource.SHARED); + String value = selectEntryResultSet.getString("VALUE"); + if (value != null) { + bibEntry.setField(FieldFactory.parseField(selectEntryResultSet.getString("NAME")), value, EntryEventSource.SHARED); + } } } catch (SQLException e) { LOGGER.error("SQL Error", e); diff --git a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java index 7a055dec41f..f93c3193342 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java +++ b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java @@ -30,7 +30,6 @@ import org.jabref.model.entry.event.EntryEvent; import org.jabref.model.entry.event.EntryEventSource; import org.jabref.model.entry.event.FieldChangedEvent; -import org.jabref.model.entry.field.Field; import org.jabref.model.metadata.MetaData; import org.jabref.model.metadata.event.MetaDataChangedEvent; import org.jabref.model.util.FileUpdateMonitor; @@ -41,8 +40,8 @@ import org.slf4j.LoggerFactory; /** - * Synchronizes the shared or local databases with their opposite side. - * Local changes are pushed by {@link EntryEvent} using Google's Guava EventBus. + * Synchronizes the shared or local databases with their opposite side. Local changes are pushed by {@link EntryEvent} + * using Google's Guava EventBus. */ public class DBMSSynchronizer implements DatabaseSynchronizer { @@ -135,11 +134,10 @@ public void listen(MetaDataChangedEvent event) { } /** - * Sets the table structure of shared database if needed and pulls all shared entries - * to the new local database. + * Sets the table structure of shared database if needed and pulls all shared entries to the new local database. * - * @throws DatabaseNotSupportedException if the version of shared database does not match - * the version of current shared database support ({@link DBMSProcessor}). + * @throws DatabaseNotSupportedException if the version of shared database does not match the version of current + * shared database support ({@link DBMSProcessor}). */ public void initializeDatabases() throws DatabaseNotSupportedException { try { @@ -163,8 +161,8 @@ public void initializeDatabases() throws DatabaseNotSupportedException { } /** - * Synchronizes the local database with shared one. - * Possible update types are removal, update or insert of a {@link BibEntry}. + * Synchronizes the local database with shared one. Possible update types are removal, update or insert of a {@link + * BibEntry}. */ @Override public void synchronizeLocalDatabase() { @@ -191,17 +189,17 @@ public void synchronizeLocalDatabase() { localEntry.setType(sharedEntry.get().getType(), EntryEventSource.SHARED); localEntry.getSharedBibEntryData() .setVersion(sharedEntry.get().getSharedBibEntryData().getVersion()); - for (Field field : sharedEntry.get().getFields()) { - localEntry.setField(field, sharedEntry.get().getField(field), EntryEventSource.SHARED); - } - - Set redundantLocalEntryFields = localEntry.getFields(); - redundantLocalEntryFields.removeAll(sharedEntry.get().getFields()); - - // remove not existing fields - for (Field redundantField : redundantLocalEntryFields) { - localEntry.clearField(redundantField, EntryEventSource.SHARED); - } + // copy remote values to local entry + sharedEntry.get().getFieldMap().forEach( + (field, value) -> localEntry.setField(field, value, EntryEventSource.SHARED) + ); + + // locally remove not existing fields + localEntry.getFields().stream() + .filter(field -> !sharedEntry.get().hasField(field)) + .forEach( + field -> localEntry.clearField(field, EntryEventSource.SHARED) + ); } } } @@ -220,7 +218,7 @@ public void synchronizeLocalDatabase() { * Removes all local entries which are not present on shared database. * * @param localEntries List of {@link BibEntry} the entries should be removed from - * @param sharedIDs Set of all IDs which are present on shared database + * @param sharedIDs Set of all IDs which are present on shared database */ private void removeNotSharedEntries(List localEntries, Set sharedIDs) { for (int i = 0; i < localEntries.size(); i++) { @@ -325,8 +323,8 @@ public void pullChanges() { } /** - * Checks whether the current SQL connection is valid. - * In case that the connection is not valid a new {@link ConnectionLostEvent} is going to be sent. + * Checks whether the current SQL connection is valid. In case that the connection is not valid a new {@link + * ConnectionLostEvent} is going to be sent. * * @return true if the connection is valid, else false. */ @@ -337,7 +335,6 @@ public boolean checkCurrentConnection() { eventBus.post(new ConnectionLostEvent(bibDatabaseContext)); } return isValid; - } catch (SQLException e) { LOGGER.error("SQL Error:", e); return false; @@ -348,7 +345,8 @@ public boolean checkCurrentConnection() { * Checks whether the {@link EntryEventSource} of an {@link EntryEvent} is crucial for this class. * * @param event An {@link EntryEvent} - * @return true if the event is able to trigger operations in {@link DBMSSynchronizer}, else false + * @return true if the event is able to trigger operations in {@link DBMSSynchronizer}, else + * false */ public boolean isEventSourceAccepted(EntryEvent event) { EntryEventSource eventSource = event.getEntryEventSource(); diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index de36489fc52..4212c48cc99 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -42,6 +42,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Represents a BibTex / BibLaTeX entry. + * + * In case you search for a builder as described in Item 2 of the book "Effective Java", you won't find one. Please use the methods {@link #withCiteKey(String)} and {@link #withField(Field, String)}. + */ public class BibEntry implements Cloneable { public static final EntryType DEFAULT_TYPE = StandardEntryType.Misc; @@ -63,6 +68,7 @@ public class BibEntry implements Cloneable { private ObservableMap fields = FXCollections.observableMap(new ConcurrentHashMap<>()); private String parsedSerialization = ""; private String commentsBeforeEntry = ""; + /** * Marks whether the complete serialization, which was read from file, should be used. * @@ -75,7 +81,6 @@ public class BibEntry implements Cloneable { */ public BibEntry() { this(IdGenerator.next(), DEFAULT_TYPE); - } /** @@ -266,8 +271,9 @@ public String getId() { } /** - * Sets this entry's ID, provided the database containing it - * doesn't veto the change. + * Sets this entry's identifier (ID). It is used internally to distinguish different BibTeX entries. It is not the BibTeX key. The BibTexKey is the {@link InternalField.KEY_FIELD}. + * + * The entry is also updated in the shared database - provided the database containing it doesn't veto the change. * * @param id The ID to be used */ @@ -523,13 +529,6 @@ public Optional setField(Field field, String value, EntryEventSourc return Optional.of(change); } - public Optional setField(Field field, Optional value, EntryEventSource eventSource) { - if (value.isPresent()) { - return setField(field, value.get(), eventSource); - } - return Optional.empty(); - } - /** * Set a field, and notify listeners about the change. * diff --git a/src/main/java/org/jabref/model/entry/field/InternalField.java b/src/main/java/org/jabref/model/entry/field/InternalField.java index b7025f8f8aa..2338da7fa1b 100644 --- a/src/main/java/org/jabref/model/entry/field/InternalField.java +++ b/src/main/java/org/jabref/model/entry/field/InternalField.java @@ -10,16 +10,17 @@ * JabRef internal fields */ public enum InternalField implements Field { - INTERNAL_ALL_FIELD("all"), - INTERNAL_ALL_TEXT_FIELDS_FIELD("all-text-fields"), - MARKED_INTERNAL("__markedentry"), OWNER("owner"), TIMESTAMP("timestamp", FieldProperty.DATE), GROUPS("groups"), KEY_FIELD("bibtexkey"), TYPE_HEADER("entrytype"), OBSOLETE_TYPE_HEADER("bibtextype"), - BIBTEX_STRING("__string"), + MARKED_INTERNAL("__markedentry"), // used in old versions of JabRef. Currently used for conversion only + // all field names starting with "Jabref-internal-" are not appearing in .bib files + BIBTEX_STRING("__string"), // marker that the content is just a BibTeX string + INTERNAL_ALL_FIELD("all"), // virtual field to denote "all fields". Used in the meta data serialiization for save actions. + INTERNAL_ALL_TEXT_FIELDS_FIELD("all-text-fields"), // virtual field to denote "all text fields". Used in the meta data serialiization for save actions. INTERNAL_ID_FIELD("JabRef-internal-id"); private final String name; diff --git a/src/main/resources/csl-locales b/src/main/resources/csl-locales index 9785a6e3584..e89e6b08b5c 160000 --- a/src/main/resources/csl-locales +++ b/src/main/resources/csl-locales @@ -1 +1 @@ -Subproject commit 9785a6e3584e8c903df3d5a53f0e800dcabd282b +Subproject commit e89e6b08b5c621a414fc7114f2129efac5f8c7d5 diff --git a/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java b/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java index c8171a403c0..b2e6183c8ed 100644 --- a/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java +++ b/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java @@ -108,30 +108,30 @@ void writeEntryWithFile() throws Exception { @Test void writeEntryWithOrField() throws Exception { - StringWriter stringWriter = new StringWriter(); - - BibEntry entry = new BibEntry(StandardEntryType.InBook); - //set an required OR field (author/editor) - entry.setField(StandardField.EDITOR, "Foo Bar"); - entry.setField(StandardField.JOURNAL, "International Journal of Something"); - //set an optional field - entry.setField(StandardField.NUMBER, "1"); - entry.setField(StandardField.NOTE, "some note"); - - writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX); - - String actual = stringWriter.toString(); - - // @formatter:off - String expected = OS.NEWLINE + "@InBook{," + OS.NEWLINE + - " editor = {Foo Bar}," + OS.NEWLINE + - " note = {some note}," + OS.NEWLINE + - " number = {1}," + OS.NEWLINE + - " journal = {International Journal of Something}," + OS.NEWLINE + - "}" + OS.NEWLINE; - // @formatter:on - - assertEquals(expected, actual); + StringWriter stringWriter = new StringWriter(); + + BibEntry entry = new BibEntry(StandardEntryType.InBook); + //set an required OR field (author/editor) + entry.setField(StandardField.EDITOR, "Foo Bar"); + entry.setField(StandardField.JOURNAL, "International Journal of Something"); + //set an optional field + entry.setField(StandardField.NUMBER, "1"); + entry.setField(StandardField.NOTE, "some note"); + + writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX); + + String actual = stringWriter.toString(); + + // @formatter:off + String expected = OS.NEWLINE + "@InBook{," + OS.NEWLINE + + " editor = {Foo Bar}," + OS.NEWLINE + + " note = {some note}," + OS.NEWLINE + + " number = {1}," + OS.NEWLINE + + " journal = {International Journal of Something}," + OS.NEWLINE + + "}" + OS.NEWLINE; + // @formatter:on + + assertEquals(expected, actual); } @Test @@ -426,6 +426,36 @@ void monthFieldSpecialSyntax() throws IOException { assertEquals(bibtexEntry, actual); } + @Test + void constantMonthApril() throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Misc) + .withField(StandardField.MONTH, "#apr#"); + + StringWriter stringWriter = new StringWriter(); + writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX); + + assertEquals(OS.NEWLINE + + "@Misc{," + OS.NEWLINE + + " month = apr," + OS.NEWLINE + + "}" + OS.NEWLINE, + stringWriter.toString()); + } + + @Test + void monthApril() throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Misc) + .withField(StandardField.MONTH, "apr"); + + StringWriter stringWriter = new StringWriter(); + writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX); + + assertEquals(OS.NEWLINE + + "@Misc{," + OS.NEWLINE + + " month = {apr}," + OS.NEWLINE + + "}" + OS.NEWLINE, + stringWriter.toString()); + } + @Test void addFieldWithLongerLength() throws IOException { // @formatter:off diff --git a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java index 876995c42ef..0ec73d0ceac 100644 --- a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java +++ b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java @@ -289,7 +289,24 @@ void writeEntryWithCustomizedTypeAlsoWritesTypeDeclaration() throws Exception { } @Test - void roundtrip() throws Exception { + void roundtripWithArticleMonths() throws Exception { + Path testBibtexFile = Paths.get("src/test/resources/testbib/articleWithMonths.bib"); + Charset encoding = StandardCharsets.UTF_8; + ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(Importer.getReader(testBibtexFile, encoding)); + + when(preferences.getEncoding()).thenReturn(encoding); + when(preferences.isSaveInOriginalOrder()).thenReturn(true); + BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData(), + new Defaults(BibDatabaseMode.BIBTEX)); + + databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); + try (Scanner scanner = new Scanner(testBibtexFile, encoding.name())) { + assertEquals(scanner.useDelimiter("\\A").next(), stringWriter.toString()); + } + } + + @Test + void roundtripWithComplexBib() throws Exception { Path testBibtexFile = Paths.get("src/test/resources/testbib/complex.bib"); Charset encoding = StandardCharsets.UTF_8; ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(Importer.getReader(testBibtexFile, encoding)); 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 71b452cdd8b..1dcd5ba75c8 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java @@ -26,6 +26,7 @@ import org.jabref.model.entry.BibEntryType; import org.jabref.model.entry.BibtexString; import org.jabref.model.entry.Date; +import org.jabref.model.entry.Month; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.FieldPriority; import org.jabref.model.entry.field.InternalField; @@ -1711,4 +1712,76 @@ void parseYear() throws Exception { assertEquals(new Date(2003), result.get().getPublicationDate().get()); } + + @Test + void parseEntryUsingStringConstantsForTwoAuthorsWithEtAsStringConstant() throws ParseException { + // source of the example: https://github.com/JabRef/help.jabref.org/blob/gh-pages/en/Strings.md + Collection parsed = parser + .parseEntries("@String { kopp = \"Kopp, Oliver\" }" + + "@String { kubovy = \"Kubovy, Jan\" }" + + "@String { et = \" and \" }" + + "@Misc{m1, author = kopp # et # kubovy }" ); + + BibEntry expectedEntry = new BibEntry(StandardEntryType.Misc) + .withCiteKey("m1") + .withField(StandardField.AUTHOR, "#kopp##et##kubovy#"); + + assertEquals(List.of(expectedEntry), parsed); + } + + @Test + void parseStringConstantsForTwoAuthorsHasCorrectBibTeXEntry() throws ParseException { + // source of the example: https://github.com/JabRef/help.jabref.org/blob/gh-pages/en/Strings.md + Collection parsed = parser + .parseEntries("@String { kopp = \"Kopp, Oliver\" }" + + "@String { kubovy = \"Kubovy, Jan\" }" + + "@String { et = \" and \" }" + + "@Misc{m2, author = kopp # \" and \" # kubovy }" ); + + BibEntry expectedEntry = new BibEntry(StandardEntryType.Misc) + .withCiteKey("m2") + .withField(StandardField.AUTHOR, "#kopp# and #kubovy#"); + + assertEquals(List.of(expectedEntry), parsed); + } + + @Test + void parseStringConstantsForTwoAuthors() throws ParseException { + // source of the example: https://github.com/JabRef/help.jabref.org/blob/gh-pages/en/Strings.md + Collection parsed = parser + .parseEntries("@String { kopp = \"Kopp, Oliver\" }" + + "@String { kubovy = \"Kubovy, Jan\" }" + + "@String { et = \" and \" }" + + "@Misc{m2, author = kopp # \" and \" # kubovy }" ); + + assertEquals("#kopp# and #kubovy#", parsed.iterator().next().getField(StandardField.AUTHOR).get()); + } + + @Test + void textAprilIsParsedAsMonthApril() throws ParseException { + Optional result = parser.parseSingleEntry("@Misc{m, month = \"apr\" }" ); + + assertEquals(Month.APRIL, result.get().getMonth().get()); + } + + @Test + void textAprilIsDisplayedAsConstant() throws ParseException { + Optional result = parser.parseSingleEntry("@Misc{m, month = \"apr\" }" ); + + assertEquals("apr", result.get().getField(StandardField.MONTH).get()); + } + + @Test + void bibTeXConstantAprilIsParsedAsMonthApril() throws ParseException { + Optional result = parser.parseSingleEntry("@Misc{m, month = apr }" ); + + assertEquals(Month.APRIL, result.get().getMonth().get()); + } + + @Test + void bibTeXConstantAprilIsDisplayedAsConstant() throws ParseException { + Optional result = parser.parseSingleEntry("@Misc{m, month = apr }" ); + + assertEquals("#apr#", result.get().getField(StandardField.MONTH).get()); + } } diff --git a/src/test/java/org/jabref/model/entry/BibtexStringTest.java b/src/test/java/org/jabref/model/entry/BibtexStringTest.java index 1341ac43b33..c6b251410f4 100644 --- a/src/test/java/org/jabref/model/entry/BibtexStringTest.java +++ b/src/test/java/org/jabref/model/entry/BibtexStringTest.java @@ -9,35 +9,117 @@ public class BibtexStringTest { @Test - public void test() { + public void initalizationWorksCorrectly() { // Instantiate BibtexString bs = new BibtexString("AAA", "An alternative action"); - bs.setId("ID"); - assertEquals("ID", bs.getId()); assertEquals("AAA", bs.getName()); assertEquals("An alternative action", bs.getContent()); assertEquals(BibtexString.Type.OTHER, bs.getType()); + } + + @Test + public void idIsUpdatedAtSetId() { + // Instantiate + BibtexString bs = new BibtexString("AAA", "An alternative action"); + bs.setId("ID"); + assertEquals("ID", bs.getId()); + } + + @Test + public void cloningDoesNotChangeContents() { + BibtexString bs = new BibtexString("AAA", "An alternative action"); + bs.setId("ID"); - // Clone BibtexString bs2 = (BibtexString) bs.clone(); + assertEquals(bs.getId(), bs2.getId()); assertEquals(bs.getName(), bs2.getName()); assertEquals(bs.getContent(), bs2.getContent()); assertEquals(bs.getType(), bs2.getType()); + } + + @Test + public void clonedBibtexStringEqualsOriginalString() { + BibtexString bibtexString = new BibtexString("AAA", "An alternative action"); + bibtexString.setId("ID"); + + BibtexString clone = (BibtexString) bibtexString.clone(); + + assertEquals(bibtexString, clone); + } - // Change cloned BibtexString + @Test + public void usingTheIdGeneratorDoesNotHitTheOriginalId() { + // Instantiate + BibtexString bs = new BibtexString("AAA", "An alternative action"); + bs.setId("ID"); + BibtexString bs2 = (BibtexString) bs.clone(); bs2.setId(IdGenerator.next()); assertNotEquals(bs.getId(), bs2.getId()); + } + + @Test + public void settingFieldsInACloneWorks() { + // Instantiate + BibtexString bs = new BibtexString("AAA", "An alternative action"); + bs.setId("ID"); + BibtexString bs2 = (BibtexString) bs.clone(); + + bs2.setId(IdGenerator.next()); bs2.setName("aOG"); assertEquals(BibtexString.Type.AUTHOR, bs2.getType()); + bs2.setContent("Oscar Gustafsson"); assertEquals("aOG", bs2.getName()); assertEquals("Oscar Gustafsson", bs2.getContent()); } + @Test + public void modifyingACloneDoesNotModifyTheOriginalEntry() { + // Instantiate + BibtexString original = new BibtexString("AAA", "An alternative action"); + original.setId("ID"); + + BibtexString clone = (BibtexString) original.clone(); + clone.setId(IdGenerator.next()); + clone.setName("aOG"); + clone.setContent("Oscar Gustafsson"); + + assertEquals("AAA", original.getName()); + assertEquals("An alternative action", original.getContent()); + } + @Test public void getContentNeverReturnsNull() { BibtexString bs = new BibtexString("SomeName", null); assertNotNull(bs.getContent()); } + + @Test + public void authorTypeCorrectlyDetermined() { + // Source of the example: https://help.jabref.org/en/Strings + BibtexString bibtexString = new BibtexString("aKopp", "KoppOliver"); + assertEquals(BibtexString.Type.AUTHOR, bibtexString.getType()); + } + + @Test + public void institutionTypeCorrectlyDetermined() { + // Source of the example: https://help.jabref.org/en/Strings + BibtexString bibtexString = new BibtexString("iMIT", "{Massachusetts Institute of Technology ({MIT})}"); + assertEquals(BibtexString.Type.INSTITUTION, bibtexString.getType()); + } + + @Test + public void otherTypeCorrectlyDeterminedForLowerCase() { + // Source of the example: https://help.jabref.org/en/Strings + BibtexString bibtexString = new BibtexString("anct", "Anecdote"); + assertEquals(BibtexString.Type.OTHER, bibtexString.getType()); + } + + @Test + public void otherTypeCorrectlyDeterminedForUpperCase() { + // Source of the example: https://help.jabref.org/en/Strings + BibtexString bibtexString = new BibtexString("lTOSCA", "Topology and Orchestration Specification for Cloud Applications"); + assertEquals(BibtexString.Type.OTHER, bibtexString.getType()); + } } diff --git a/src/test/resources/testbib/articleWithMonths.bib b/src/test/resources/testbib/articleWithMonths.bib new file mode 100644 index 00000000000..6898d754ade --- /dev/null +++ b/src/test/resources/testbib/articleWithMonths.bib @@ -0,0 +1,10 @@ +% Encoding: UTF-8 +@Article{constant, + month = apr +} + +@Article{text, + month = {apr} +} + +@Comment{jabref-meta: databaseType:bibtex;}