Skip to content

Commit 0f926c6

Browse files
timurturbilkoppor
andauthored
Support BibLaTeX datamodel validations (#13693)
* support BibLaTeX datamodel validations * correct typo * change JavaDoc comments to markdown style and use lowercase for biblatex and bibtex in variable names instead of camel case. * update CHANGELOG.md * update CHANGELOG.md * Refine parsing logic * Fix JavaDoc comment syntax --------- Co-authored-by: Oliver Kopp <[email protected]>
1 parent 0883d49 commit 0f926c6

File tree

3 files changed

+114
-39
lines changed

3 files changed

+114
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
3232
- When relativizing file names, symlinks are now taken into account. [#12995](https://github.com/JabRef/jabref/issues/12995)
3333
- We added a new button for shortening the DOI near the DOI field in the general tab when viewing an entry. [#13639](https://github.com/JabRef/jabref/issues/13639)
3434
- We added support for finding CSL-Styles based on their short title (e.g. apa instead of "american psychological association"). [#13728](https://github.com/JabRef/jabref/pull/13728)
35+
- We added BibLaTeX datamodel validation support in order to improve error message quality in entries' fields validation. [#13318](https://github.com/JabRef/jabref/issues/13318)
3536

3637
### Changed
3738

jablib/src/main/java/org/jabref/logic/biblog/BibtexLogParser.java

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.io.IOException;
44
import java.nio.file.Files;
55
import java.nio.file.Path;
6-
import java.util.ArrayList;
76
import java.util.List;
87
import java.util.Optional;
98
import java.util.regex.Matcher;
@@ -19,52 +18,88 @@
1918
* Parses the contents of a .blg (BibTeX log) file to extract warning messages.
2019
*/
2120
public class BibtexLogParser {
22-
private static final Pattern WARNING_PATTERN = Pattern.compile("^Warning--(?<message>[a-zA-Z ]+) in (?<entryKey>[^\\s]+)$");
21+
private static final Pattern BIBTEX_WARNING_PATTERN = Pattern.compile("^Warning--(?<message>[a-zA-Z ]+) in (?<entryKey>[^\\s]+)$");
22+
private static final Pattern BIBLATEX_WARNING_PATTERN = Pattern.compile(
23+
"(?:(?:\\[\\d+\\] )?Biber\\.pm:\\d+> )?WARN - Datamodel: [a-z]+ entry '(?<entryKey>[^']+)' \\((?<fileName>[^)]+)\\): (?<message>.+)");
24+
2325
private static final String EMPTY_FIELD_PREFIX = "empty";
26+
private static final String INVALID_FIELD_PREFIX = "field '";
27+
private static final String MULTI_INVALID_FIELD_PREFIX = "field - one of '";
2428

2529
public List<BibWarning> parseBiblog(@NonNull Path blgFilePath) throws IOException {
26-
List<BibWarning> warnings = new ArrayList<>();
27-
List<String> lines = Files.readAllLines(blgFilePath);
28-
for (String line : lines) {
29-
Optional<BibWarning> potentialWarning = parseWarningLine(line);
30-
potentialWarning.ifPresent(warnings::add);
31-
}
32-
return warnings;
30+
return Files.lines(blgFilePath)
31+
.map(this::parseWarningLine)
32+
.flatMap(Optional::stream)
33+
.toList();
3334
}
3435

35-
/**
36-
* Parses a single line from the .blg file to identify a warning.
37-
* <p>
38-
* Currently supports parsing warnings of the format:
39-
* <pre>
40-
* Warning--[message] in [entryKey]
41-
* </pre>
42-
* For example: {@code Warning--empty journal in Scholey_2013}
43-
*
44-
* @param line a single line from the .blg file
45-
* @return an Optional containing a {@link BibWarning} if a match is found, or empty otherwise
46-
*/
47-
private Optional<BibWarning> parseWarningLine(String line) {
48-
// TODO: Support additional warning formats
49-
Matcher matcher = WARNING_PATTERN.matcher(line);
50-
if (!matcher.find()) {
51-
return Optional.empty();
36+
/// Parses a single line from a .blg file to identify a warning.
37+
///
38+
/// This method supports two warning formats:
39+
///
40+
/// 1. **BibTeX Warnings:** Simple warnings from the legacy BibTeX backend.
41+
/// `Warning--[message] in [entryKey]`
42+
/// For example: `Warning--empty journal in Scholey_2013`
43+
///
44+
/// 2. **BibLaTeX Datamodel Warnings:** Detailed warnings from the Biber backend, including datamodel validation issues.
45+
/// `[Log line] > WARN - Datamodel: [entry type] entry '[entryKey]' ([fileName]): [message]`
46+
/// For example: `Biber.pm:123> WARN - Datamodel: article entry 'Scholey_2013' (file.bib): Invalid field 'journal'`
47+
///
48+
/// @param line The single line from the .blg file to parse.
49+
///
50+
/// @returns An `Optional` containing a `BibWarning` if a match is found, or an empty `Optional` otherwise.
51+
Optional<BibWarning> parseWarningLine(String line) {
52+
Matcher bibtexMatcher = BIBTEX_WARNING_PATTERN.matcher(line);
53+
if (bibtexMatcher.find()) {
54+
String message = bibtexMatcher.group("message").trim();
55+
String entryKey = bibtexMatcher.group("entryKey");
56+
// Extract field name for warnings related to empty fields (e.g., "empty journal" -> fieldName = "journal")
57+
String fieldName = null;
58+
if (message.startsWith(EMPTY_FIELD_PREFIX)) {
59+
fieldName = message.substring(EMPTY_FIELD_PREFIX.length()).trim();
60+
fieldName = FieldFactory.parseField(fieldName).getName();
61+
}
62+
63+
return Optional.of(new BibWarning(
64+
SeverityType.WARNING,
65+
message,
66+
fieldName,
67+
entryKey
68+
));
5269
}
5370

54-
String message = matcher.group("message").trim();
55-
String entryKey = matcher.group("entryKey");
56-
// Extract field name for warnings related to empty fields (e.g., "empty journal" -> fieldName = "journal")
57-
String fieldName = null;
58-
if (message.startsWith(EMPTY_FIELD_PREFIX)) {
59-
fieldName = message.substring(EMPTY_FIELD_PREFIX.length()).trim();
60-
fieldName = FieldFactory.parseField(fieldName).getName();
71+
Matcher biblatexMatcher = BIBLATEX_WARNING_PATTERN.matcher(line);
72+
if (biblatexMatcher.find()) {
73+
String message = biblatexMatcher.group("message").trim();
74+
String entryKey = biblatexMatcher.group("entryKey");
75+
String fieldName = null;
76+
77+
// Extract field name for warnings related to invalid fields (e.g., "Invalid field 'publisher' for entrytype 'article'" -> fieldName = "publisher")
78+
String lowerCaseMessage = message.toLowerCase();
79+
if (lowerCaseMessage.contains(INVALID_FIELD_PREFIX)) {
80+
int startIndex = lowerCaseMessage.indexOf(INVALID_FIELD_PREFIX) + INVALID_FIELD_PREFIX.length();
81+
int endIndex = lowerCaseMessage.indexOf('\'', startIndex);
82+
if (endIndex != -1) {
83+
fieldName = lowerCaseMessage.substring(startIndex, endIndex).trim();
84+
fieldName = FieldFactory.parseField(fieldName).getName();
85+
}
86+
} else if (lowerCaseMessage.contains(MULTI_INVALID_FIELD_PREFIX)) {
87+
int startIndex = lowerCaseMessage.indexOf(MULTI_INVALID_FIELD_PREFIX) + MULTI_INVALID_FIELD_PREFIX.length();
88+
int endIndex = lowerCaseMessage.indexOf('\'', startIndex);
89+
if (endIndex != -1) {
90+
fieldName = lowerCaseMessage.substring(startIndex, endIndex).trim().split(",")[0].trim();
91+
fieldName = FieldFactory.parseField(fieldName).getName();
92+
}
93+
}
94+
95+
return Optional.of(new BibWarning(
96+
SeverityType.WARNING,
97+
message,
98+
fieldName,
99+
entryKey
100+
));
61101
}
62102

63-
return Optional.of(new BibWarning(
64-
SeverityType.WARNING,
65-
message,
66-
fieldName,
67-
entryKey
68-
));
103+
return Optional.empty();
69104
}
70105
}

jablib/src/test/java/org/jabref/logic/biblog/BibtexLogParserTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
import java.io.IOException;
44
import java.nio.file.Path;
55
import java.util.List;
6+
import java.util.Optional;
7+
import java.util.stream.Stream;
68

79
import org.jabref.model.biblog.BibWarning;
810
import org.jabref.model.biblog.SeverityType;
911

1012
import org.junit.jupiter.api.BeforeEach;
1113
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.params.ParameterizedTest;
15+
import org.junit.jupiter.params.provider.Arguments;
16+
import org.junit.jupiter.params.provider.MethodSource;
1217

1318
import static org.junit.jupiter.api.Assertions.assertEquals;
1419

@@ -31,4 +36,38 @@ void parsesWarningsFromResourceFileTest() throws IOException {
3136
new BibWarning(SeverityType.WARNING, "empty year", "year", "Tan_2021")
3237
), warnings);
3338
}
39+
40+
@ParameterizedTest
41+
@MethodSource("biblatexValidationWarningsProvider")
42+
void parsesBiblatexValidationWarnings(String warningLine, Optional<BibWarning> expectedWarning) {
43+
assertEquals(expectedWarning, parser.parseWarningLine(warningLine));
44+
}
45+
46+
private static Stream<Arguments> biblatexValidationWarningsProvider() {
47+
return Stream.of(
48+
Arguments.of("[1124] Biber.pm:131> WARN - Datamodel: article entry 'Corti_2009' (chocolate.bib): Invalid field 'publisher' for entrytype 'article'",
49+
Optional.of(new BibWarning(SeverityType.WARNING, "Invalid field 'publisher' for entrytype 'article'", "publisher", "Corti_2009"))),
50+
51+
Arguments.of("[1126] Biber.pm:131> WARN - Datamodel: article entry 'Parker_2006' (Chocolate.bib): Missing mandatory field - one of 'date, year' must be defined",
52+
Optional.of(new BibWarning(SeverityType.WARNING, "Missing mandatory field - one of 'date, year' must be defined", "date", "Parker_2006"))),
53+
54+
Arguments.of("[1127] Biber.pm:131> WARN - Datamodel: article entry 'Corti_2009' (Chocolate.bib): Missing mandatory field 'author'",
55+
Optional.of(new BibWarning(SeverityType.WARNING, "Missing mandatory field 'author'", "author", "Corti_2009"))),
56+
57+
Arguments.of("[1128] Biber.pm:131> WARN - Datamodel: article entry 'Cooper_2007' (Chocolate.bib): Invalid ISSN in value of field 'issn'",
58+
Optional.of(new BibWarning(SeverityType.WARNING, "Invalid ISSN in value of field 'issn'", "issn", "Cooper_2007"))),
59+
60+
Arguments.of("[1129] Biber.pm:131> WARN - Datamodel: article entry 'Katz_2011' (Chocolate.bib): Invalid value of field 'volume' must be datatype 'integer' - ignoring field",
61+
Optional.of(new BibWarning(SeverityType.WARNING, "Invalid value of field 'volume' must be datatype 'integer' - ignoring field", "volume", "Katz_2011"))),
62+
63+
Arguments.of("WARN - Datamodel: article entry 'Keen_2001' (Chocolate.bib): Invalid field 'publisher' for entrytype 'article'",
64+
Optional.of(new BibWarning(SeverityType.WARNING, "Invalid field 'publisher' for entrytype 'article'", "publisher", "Keen_2001"))),
65+
66+
Arguments.of("WARN - Datamodel: article entry 'Macht_2007' (Chocolate.bib): Field 'groups' invalid in data model - ignoring",
67+
Optional.of(new BibWarning(SeverityType.WARNING, "Field 'groups' invalid in data model - ignoring", "groups", "Macht_2007"))),
68+
69+
Arguments.of("This is not a valid warning line", Optional.empty()),
70+
Arguments.of("", Optional.empty())
71+
);
72+
}
3473
}

0 commit comments

Comments
 (0)