Skip to content

Commit 5c73783

Browse files
authored
Merge pull request #9677 from JabRef/fix-9668
Fix parsing of backslashes at the end of field content
2 parents df7326d + 6ee08f3 commit 5c73783

File tree

8 files changed

+196
-14
lines changed

8 files changed

+196
-14
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve
2323
### Changed
2424

2525
- 'Get full text' now also checks the file url. [#568](https://github.com/koppor/jabref/issues/568)
26+
- `log.txt` now contains an entry if a BibTeX entry could not be parsed.
27+
- JabRef writes a new backup file only if there is a change. Before, JabRef created a backup upon start. [#9679](https://github.com/JabRef/jabref/pull/9679)
2628
- We modified the `Add Group` dialog to use the most recently selected group hierarchical context. [#9141](https://github.com/JabRef/jabref/issues/9141)
2729
- We refined the 'main directory not found' error message. [#9625](https://github.com/JabRef/jabref/pull/9625)
2830
- JabRef writes a new backup file only if there is a change. Before, JabRef created a backup upon start. [#9679](https://github.com/JabRef/jabref/pull/9679)
@@ -43,6 +45,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve
4345
- We fixed an issue where the command line export using `--exportMatches` flag does not create an output bib file [#9581](https://github.com/JabRef/jabref/issues/9581)
4446
- We fixed an issue where custom field in the custom entry types could not be set to mulitline [#9609](https://github.com/JabRef/jabref/issues/9609)
4547
- We fixed an issue where the Office XML exporter did not resolve BibTeX-Strings when exporting entries [forum#3741](https://discourse.jabref.org/t/exporting-bibtex-constant-strings-to-ms-office-2007-xml/3741)
48+
- JabRef is now more relaxed when parsing field content: In case a field content ended with `\`, the combination `\}` was treated as plain `}`. [#9668](https://github.com/JabRef/jabref/issues/9668)
4649

4750

4851
### Removed

src/main/java/org/jabref/gui/ClipBoardManager.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static String getContents() {
8787
return result;
8888
}
8989

90-
public Optional<String> getBibTeXEntriesFromClipbaord() {
90+
public Optional<String> getBibTeXEntriesFromClipboard() {
9191
return Optional.ofNullable(clipboard.getContent(DragAndDropDataFormats.ENTRIES)).map(String.class::cast);
9292
}
9393

src/main/java/org/jabref/gui/externalfiles/ImportHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ private void generateKeys(List<BibEntry> entries) {
258258
}
259259

260260
public List<BibEntry> handleBibTeXData(String entries) {
261-
BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), Globals.getFileUpdateMonitor());
261+
BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), fileUpdateMonitor);
262262
try {
263263
return parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8)));
264264
} catch (ParseException ex) {

src/main/java/org/jabref/gui/maintable/MainTable.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,9 @@ private void clearAndSelectLast() {
322322

323323
public void paste() {
324324
List<BibEntry> entriesToAdd;
325-
entriesToAdd = this.clipBoardManager.getBibTeXEntriesFromClipbaord()
325+
entriesToAdd = this.clipBoardManager.getBibTeXEntriesFromClipboard()
326326
.map(importHandler::handleBibTeXData)
327-
.orElseGet(this::handleNonBibteXStringData);
327+
.orElseGet(this::handleNonBibTeXStringData);
328328

329329
for (BibEntry entry : entriesToAdd) {
330330
importHandler.importEntryWithDuplicateCheck(database, entry);
@@ -334,7 +334,7 @@ public void paste() {
334334
}
335335
}
336336

337-
private List<BibEntry> handleNonBibteXStringData() {
337+
private List<BibEntry> handleNonBibTeXStringData() {
338338
String data = ClipBoardManager.getContents();
339339
List<BibEntry> entries = new ArrayList<>();
340340
try {

src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java

+48-7
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,10 @@ private void parseAndAddEntry(String type) {
278278

279279
database.insertEntry(entry);
280280
} catch (IOException ex) {
281-
// Trying to make the parser more robust.
281+
// This makes the parser more robust:
282282
// If an exception is thrown when parsing an entry, drop the entry and try to resume parsing.
283283

284-
LOGGER.debug("Could not parse entry", ex);
284+
LOGGER.warn("Could not parse entry", ex);
285285
parserResult.addWarning(Localization.lang("Error occurred when parsing entry") + ": '" + ex.getMessage()
286286
+ "'. " + "\n\n" + Localization.lang("JabRef skipped the entry."));
287287
}
@@ -290,7 +290,7 @@ private void parseAndAddEntry(String type) {
290290
private void parseJabRefComment(Map<String, String> meta) {
291291
StringBuilder buffer;
292292
try {
293-
buffer = parseBracketedTextExactly();
293+
buffer = parseBracketedFieldContent();
294294
} catch (IOException e) {
295295
// if we get an IO Exception here, then we have an unbracketed comment,
296296
// which means that we should just return and the comment will be picked up as arbitrary text
@@ -500,6 +500,16 @@ private int peek() throws IOException {
500500
return character;
501501
}
502502

503+
private char[] peekTwoCharacters() throws IOException {
504+
char character1 = (char) read();
505+
char character2 = (char) read();
506+
unread(character2);
507+
unread(character1);
508+
return new char[] {
509+
character1, character2
510+
};
511+
}
512+
503513
private int read() throws IOException {
504514
int character = pushbackReader.read();
505515

@@ -635,7 +645,7 @@ private String parseFieldContent(Field field) throws IOException {
635645
// Value is a string enclosed in brackets. There can be pairs
636646
// of brackets inside a field, so we need to count the
637647
// brackets to know when the string is finished.
638-
StringBuilder text = parseBracketedTextExactly();
648+
StringBuilder text = parseBracketedFieldContent();
639649
value.append(fieldContentFormatter.format(text, field));
640650
} else if (Character.isDigit((char) character)) { // value is a number
641651
String number = parseTextToken();
@@ -668,7 +678,6 @@ private String parseTextToken() throws IOException {
668678
int character = read();
669679
if (character == -1) {
670680
eof = true;
671-
672681
return token.toString();
673682
}
674683

@@ -886,7 +895,11 @@ private boolean isClosingBracketNext() {
886895
}
887896
}
888897

889-
private StringBuilder parseBracketedTextExactly() throws IOException {
898+
/**
899+
* This is called if a field in the form of <code>field = {content}</code> is parsed.
900+
* The global variable <code>character</code> contains <code>{</code>.
901+
*/
902+
private StringBuilder parseBracketedFieldContent() throws IOException {
890903
StringBuilder value = new StringBuilder();
891904

892905
consume('{');
@@ -898,7 +911,35 @@ private StringBuilder parseBracketedTextExactly() throws IOException {
898911
while (true) {
899912
character = (char) read();
900913

901-
boolean isClosingBracket = (character == '}') && (lastCharacter != '\\');
914+
boolean isClosingBracket = false;
915+
if (character == '}') {
916+
if (lastCharacter == '\\') {
917+
// We hit `\}`
918+
// It could be that a user has a backslash at the end of the entry, but intended to put a file path
919+
// We want to be relaxed at that case
920+
// First described at https://github.com/JabRef/jabref/issues/9668
921+
char[] nextTwoCharacters = peekTwoCharacters();
922+
// Check for "\},\n" - Example context: ` path = {c:\temp\},\n`
923+
// On Windows, it could be "\},\r\n", thus we rely in OS.NEWLINE.charAt(0) (which returns '\r' or '\n').
924+
// In all cases, we should check for '\n' as the file could be encoded with Linux line endings on Windows.
925+
if ((nextTwoCharacters[0] == ',') && ((nextTwoCharacters[1] == OS.NEWLINE.charAt(0)) || (nextTwoCharacters[1] == '\n'))) {
926+
// We hit '\}\r` or `\}\n`
927+
// Heuristics: Unwanted escaping of }
928+
//
929+
// Two consequences:
930+
//
931+
// 1. Keep `\` as read
932+
// This is already done
933+
//
934+
// 2. Treat `}` as closing bracket
935+
isClosingBracket = true;
936+
} else {
937+
isClosingBracket = false;
938+
}
939+
} else {
940+
isClosingBracket = true;
941+
}
942+
}
902943

903944
if (isClosingBracket && (brackets == 0)) {
904945
return value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import java.util.List;
4+
5+
import javax.swing.undo.UndoManager;
6+
7+
import org.jabref.gui.DialogService;
8+
import org.jabref.gui.StateManager;
9+
import org.jabref.logic.importer.ImportFormatPreferences;
10+
import org.jabref.logic.importer.ImportFormatReader;
11+
import org.jabref.model.database.BibDatabaseContext;
12+
import org.jabref.model.entry.BibEntry;
13+
import org.jabref.model.entry.field.StandardField;
14+
import org.jabref.model.entry.types.StandardEntryType;
15+
import org.jabref.model.util.DummyFileUpdateMonitor;
16+
import org.jabref.preferences.FilePreferences;
17+
import org.jabref.preferences.PreferencesService;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.mockito.Answers;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
25+
26+
class ImportHandlerTest {
27+
28+
@Test
29+
void handleBibTeXData() {
30+
ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS);
31+
32+
PreferencesService preferencesService = mock(PreferencesService.class);
33+
when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences);
34+
when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class));
35+
36+
ImportHandler importHandler = new ImportHandler(
37+
mock(BibDatabaseContext.class),
38+
preferencesService,
39+
new DummyFileUpdateMonitor(),
40+
mock(UndoManager.class),
41+
mock(StateManager.class),
42+
mock(DialogService.class),
43+
mock(ImportFormatReader.class));
44+
45+
List<BibEntry> bibEntries = importHandler.handleBibTeXData("""
46+
@InProceedings{Wen2013,
47+
library = {Tagungen\\2013\\KWTK45\\},
48+
}
49+
""");
50+
51+
BibEntry expected = new BibEntry(StandardEntryType.InProceedings)
52+
.withCitationKey("Wen2013")
53+
.withField(StandardField.LIBRARY, "Tagungen\\2013\\KWTK45\\");
54+
55+
assertEquals(List.of(expected), bibEntries.stream().toList());
56+
}
57+
}

src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java

+57-2
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ void writeReallyUnknownTypeTest() throws Exception {
184184

185185
@Test
186186
void roundTripTest() throws IOException {
187-
// @formatter:off
188187
String bibtexEntry = """
189188
@Article{test,
190189
Author = {Foo Bar},
@@ -193,7 +192,63 @@ void roundTripTest() throws IOException {
193192
Number = {1}
194193
}
195194
""".replaceAll("\n", OS.NEWLINE);
196-
// @formatter:on
195+
196+
// read in bibtex string
197+
ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(new StringReader(bibtexEntry));
198+
Collection<BibEntry> entries = result.getDatabase().getEntries();
199+
BibEntry entry = entries.iterator().next();
200+
201+
// write out bibtex string
202+
bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX);
203+
204+
assertEquals(bibtexEntry, stringWriter.toString());
205+
}
206+
207+
@Test
208+
void roundTripKeepsFilePathWithBackslashes() throws IOException {
209+
String bibtexEntry = """
210+
@Article{,
211+
file = {Tagungen\\2013\\KWTK45},
212+
}
213+
""".replaceAll("\n", OS.NEWLINE);
214+
215+
// read in bibtex string
216+
ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(new StringReader(bibtexEntry));
217+
Collection<BibEntry> entries = result.getDatabase().getEntries();
218+
BibEntry entry = entries.iterator().next();
219+
220+
// write out bibtex string
221+
bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX);
222+
223+
assertEquals(bibtexEntry, stringWriter.toString());
224+
}
225+
226+
@Test
227+
void roundTripKeepsEscapedCharacters() throws IOException {
228+
String bibtexEntry = """
229+
@Article{,
230+
demofield = {Tagungen\\2013\\KWTK45},
231+
}
232+
""".replaceAll("\n", OS.NEWLINE);
233+
234+
// read in bibtex string
235+
ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(new StringReader(bibtexEntry));
236+
Collection<BibEntry> entries = result.getDatabase().getEntries();
237+
BibEntry entry = entries.iterator().next();
238+
239+
// write out bibtex string
240+
bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX);
241+
242+
assertEquals(bibtexEntry, stringWriter.toString());
243+
}
244+
245+
@Test
246+
void roundTripKeepsFilePathEndingWithBackslash() throws IOException {
247+
String bibtexEntry = """
248+
@Article{,
249+
file = {dir\\},
250+
}
251+
""".replaceAll("\n", OS.NEWLINE);
197252

198253
// read in bibtex string
199254
ParserResult result = new BibtexParser(importFormatPreferences, fileMonitor).parse(new StringReader(bibtexEntry));

src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java

+26
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,32 @@ void parseRecognizesAbsoluteFile() throws IOException {
607607
assertEquals(Optional.of("D:\\Documents\\literature\\Tansel-PRL2006.pdf"), entry.getField(StandardField.FILE));
608608
}
609609

610+
@Test
611+
void parseRecognizesFinalSlashAsSlash() throws Exception {
612+
ParserResult result = parser
613+
.parse(new StringReader("""
614+
@misc{,
615+
test = {wired\\},
616+
}
617+
"""));
618+
619+
assertEquals(
620+
List.of(new BibEntry()
621+
.withField(new UnknownField("test"), "wired\\")),
622+
result.getDatabase().getEntries()
623+
);
624+
}
625+
626+
/**
627+
* JabRef's heuristics is not able to parse this special case.
628+
*/
629+
@Test
630+
void parseFailsWithFinalSlashAsSlashWhenSingleLine() throws Exception {
631+
ParserResult parserResult = parser.parse(new StringReader("@misc{, test = {wired\\}}"));
632+
// In case JabRef was more relaxed, `assertFalse` would be provided here.
633+
assertTrue(parserResult.hasWarnings());
634+
}
635+
610636
@Test
611637
void parseRecognizesDateFieldWithConcatenation() throws IOException {
612638
ParserResult result = parser

0 commit comments

Comments
 (0)