Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Added

- We added the integrity check to the jabkit cli application. [#13848](https://github.com/JabRef/jabref/issues/13848)
- We added support for Cygwin-file paths on a Windows Operating System. [#13274](https://github.com/JabRef/jabref/issues/13274)
- We fixed an issue where "Print preview" would throw a `NullPointerException` if no printers were available. [#13708](https://github.com/JabRef/jabref/issues/13708)
- We added the option to enable the language server in the preferences. [#13697](https://github.com/JabRef/jabref/pull/13697)
Expand Down
2 changes: 1 addition & 1 deletion jabkit/src/main/java/org/jabref/cli/ArgumentProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
// sorted alphabetically
subcommands = {
CheckConsistency.class,
// CheckIntegrity.class,
CheckIntegrity.class,
Convert.class,
Fetch.class,
GenerateBibFromAux.class,
Expand Down
112 changes: 104 additions & 8 deletions jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,133 @@
package org.jabref.cli;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;

import org.jabref.cli.converter.CygWinPathConverter;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.integrity.IntegrityCheck;
import org.jabref.logic.integrity.IntegrityMessage;
import org.jabref.logic.journals.JournalAbbreviationLoader;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.InternalField;

import picocli.CommandLine;

import static picocli.CommandLine.Command;
import static picocli.CommandLine.Mixin;
import static picocli.CommandLine.Option;
import static picocli.CommandLine.Parameters;

@Command(name = "check-integrity", description = "Check integrity of the database.")
class CheckIntegrity implements Runnable {
class CheckIntegrity implements Callable<Integer> {

@CommandLine.ParentCommand
private ArgumentProcessor argumentProcessor;

@Mixin
private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions();

@Parameters(index = "0", converter = CygWinPathConverter.class, description = "BibTeX file to check", arity = "0..1")
@Parameters(converter = CygWinPathConverter.class, description = "BibTeX file to check", arity = "1")
private Path inputFile;

@Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "Input BibTeX file")
private Path inputOption;
@Option(names = {"--output-format"}, description = "Output format: errorformat, txt or csv", defaultValue = "errorformat")
private String outputFormat;

@Option(names = {"--output-format"}, description = "Output format: txt or csv")
private String outputFormat = "txt"; // FixMe: Default value?
@Option(names = {"--allow-integer-edition"}, description = "Allows Integer edition: true or false", defaultValue = "true")
private boolean allowIntegerEdition = true;

@Override
public void run() {
public Integer call() {
Optional<ParserResult> parserResult = ArgumentProcessor.importFile(
inputFile,
"bibtex",
argumentProcessor.cliPreferences,
sharedOptions.porcelain);
if (parserResult.isEmpty()) {
System.out.println(Localization.lang("Unable to open file '%0'.", inputFile));
return 2;
}

if (parserResult.get().isInvalid()) {
System.out.println(Localization.lang("Input file '%0' is invalid and could not be parsed.", inputFile));
return 2;
}

if (!sharedOptions.porcelain) {
System.out.println(Localization.lang("Checking integrity of '%0'.", inputFile));
System.out.flush();
}

// TODO: Implement integrity checking
BibDatabaseContext databaseContext = parserResult.get().getDatabaseContext();

IntegrityCheck integrityCheck = new IntegrityCheck(
databaseContext,
argumentProcessor.cliPreferences.getFilePreferences(),
argumentProcessor.cliPreferences.getCitationKeyPatternPreferences(),
JournalAbbreviationLoader.loadRepository(argumentProcessor.cliPreferences.getJournalAbbreviationPreferences()),
allowIntegerEdition
);

List<IntegrityMessage> messages = databaseContext.getEntries().stream()
.flatMap(entry -> {
if (!sharedOptions.porcelain) {
System.out.println(Localization.lang("Checking integrity of '%0'.", entry.getCitationKey().orElse("")));
}
return integrityCheck.checkEntry(entry).stream();
})
.toList();

return switch (outputFormat.toLowerCase(Locale.ROOT)) {
case "errorformat" ->
outputErrorFormat(messages, parserResult.get());
case "txt" ->
outputTxt(messages);
case "csv" ->
outputCsv(messages);
default -> {
System.out.println(Localization.lang("Unknown output format '%0'.", outputFormat));
yield 3;
}
};
}

private int outputCsv(List<IntegrityMessage> messages) {
System.out.println("Citation Key,Field,Message");
for (IntegrityMessage message : messages) {
String citationKey = message.entry().getCitationKey().orElse("");
String field = message.field() != null ? message.field().getDisplayName() : "";
String msg = message.message().replace("\"", "\\\"");
if (msg.contains(",")) {
msg = "\"" + msg + "\"";
}
System.out.printf("%s,%s,%s%n", citationKey, field, msg);
}
return 0;
}

private int outputTxt(List<IntegrityMessage> messages) {
messages.forEach(System.out::println);
return 0;
}

private int outputErrorFormat(List<IntegrityMessage> messages, ParserResult parserResult) {
for (IntegrityMessage message : messages) {
Map<Field, ParserResult.Range> fieldRangeMap = parserResult.getFieldRanges().getOrDefault(message.entry(), new HashMap<>());
ParserResult.Range fieldRange = fieldRangeMap.getOrDefault(message.field(), fieldRangeMap.getOrDefault(InternalField.KEY_FIELD, parserResult.getArticleRanges().getOrDefault(message.entry(), ParserResult.Range.getNullRange())));

System.out.printf("%s:%d:%d: %s\n".formatted(
inputFile,
fieldRange.startLine(),
fieldRange.startColumn(),
message.message()));
}
return 0;
}
}
21 changes: 21 additions & 0 deletions jablib/src/main/java/org/jabref/logic/importer/ParserResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -14,6 +17,7 @@
import org.jabref.model.database.BibDatabases;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryType;
import org.jabref.model.entry.field.Field;
import org.jabref.model.metadata.MetaData;

public class ParserResult {
Expand All @@ -25,6 +29,9 @@ public class ParserResult {
private boolean invalid;
private boolean changedOnMigration = false;

private final Map<BibEntry, Range> articleRanges = new IdentityHashMap<>();
private final Map<BibEntry, Map<Field, Range>> fieldRanges = new IdentityHashMap<>();

public ParserResult() {
this(List.of());
}
Expand Down Expand Up @@ -147,4 +154,18 @@ public boolean getChangedOnMigration() {
public void setChangedOnMigration(boolean wasChangedOnMigration) {
this.changedOnMigration = wasChangedOnMigration;
}

public Map<BibEntry, Map<Field, Range>> getFieldRanges() {
return fieldRanges;
}

public Map<BibEntry, Range> getArticleRanges() {
return articleRanges;
}

public record Range(int startLine, int startColumn, int endLine, int endColumn) {
public static Range getNullRange() {
return new Range(0, 0, 0, 0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.Reader;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Base64;
import java.util.Collection;
import java.util.Deque;
Expand Down Expand Up @@ -46,6 +47,7 @@
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.InternalField;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.EntryTypeFactory;
import org.jabref.model.groups.ExplicitGroup;
Expand Down Expand Up @@ -90,7 +92,7 @@
*/
public class BibtexParser implements Parser {
private static final Logger LOGGER = LoggerFactory.getLogger(BibtexParser.class);
private static final Integer LOOKAHEAD = 1024;
private static final int LOOKAHEAD = 1024;
private static final String BIB_DESK_ROOT_GROUP_NAME = "BibDeskGroups";
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
private static final int INDEX_RELATIVE_PATH_IN_PLIST = 4;
Expand All @@ -100,7 +102,13 @@ public class BibtexParser implements Parser {
private BibDatabase database;
private Set<BibEntryType> entryTypes;
private boolean eof;

private int line = 1;
private int column = 1;
// Stores the last read column of the highest column number encountered on any line so far.
// In basic JDK data structures, there is no size-limited stack. We did not want to include Apache Commons Collections only for "CircularFifoBuffer"
private final Deque<Integer> highestColumns = new ArrayDeque<>();

private ParserResult parserResult;
private final MetaDataParser metaDataParser;
private final Map<String, String> parsedBibDeskGroups;
Expand Down Expand Up @@ -634,13 +642,20 @@ private int read() throws IOException {
}
if (character == '\n') {
line++;
highestColumns.push(column);
column = 1;
} else {
column++;
}
return character;
}

private void unread(int character) throws IOException {
if (character == '\n') {
line--;
column = highestColumns.pop();
} else {
column--;
}
pushbackReader.unread(character);
if (pureTextFromFile.getLast() == character) {
Expand Down Expand Up @@ -679,13 +694,22 @@ private String parsePreamble() throws IOException {
private BibEntry parseEntry(String entryType) throws IOException {
BibEntry result = new BibEntry(EntryTypeFactory.parse(entryType));

int articleStartLine = line;
int articleStartColumn = column;

skipWhitespace();
consume('{', '(');
int character = peek();
if ((character != '\n') && (character != '\r')) {
skipWhitespace();
}
int keyStartLine = line;
int keyStartColumn = column;
String key = parseKey();

ParserResult.Range keyRange = new ParserResult.Range(keyStartLine, keyStartColumn, line, column);
parserResult.getFieldRanges().computeIfAbsent(result, _ -> new HashMap<>()).put(InternalField.KEY_FIELD, keyRange);

result.setCitationKey(key);
skipWhitespace();

Expand Down Expand Up @@ -713,10 +737,13 @@ private BibEntry parseEntry(String entryType) throws IOException {
// Consume new line which signals end of entry
skipOneNewline();

parserResult.getArticleRanges().put(result, new ParserResult.Range(articleStartLine, articleStartColumn, line, column));
return result;
}

private void parseField(BibEntry entry) throws IOException {
int startLine = line;
int startColumn = column;
Field field = FieldFactory.parseField(parseTextToken().toLowerCase(Locale.ROOT));

skipWhitespace();
Expand Down Expand Up @@ -770,6 +797,8 @@ private void parseField(BibEntry entry) throws IOException {
}
}
}
ParserResult.Range keyRange = new ParserResult.Range(startLine, startColumn, line, column);
parserResult.getFieldRanges().computeIfAbsent(entry, _ -> new HashMap<>()).put(field, keyRange);
}

private String parseFieldContent(Field field) throws IOException {
Expand Down
1 change: 1 addition & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ Unknown\ export\ format\ %0=Unknown export format %0
Importing\ %0=Importing %0
Importing\ file\ %0\ as\ unknown\ format=Importing file %0 as unknown format
Format\ used\:\ %0=Format used: %0
Unknown\ output\ format\ '%0'.=Unknown output format '%0'.

Extension=Extension

Expand Down
Loading