diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java index 5bfa504edf5..f7b2ebb58f6 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java @@ -36,6 +36,9 @@ import jakarta.inject.Inject; import org.controlsfx.control.textfield.CustomTextField; +import java.util.List; +import java.util.stream.Collectors; + /** * This class controls the user interface of the journal abbreviations dialog. The UI elements and their layout are * defined in the FXML file. @@ -59,6 +62,7 @@ public class JournalAbbreviationsTab extends AbstractPreferenceTabView cellData.getValue().nameProperty()); journalTableNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + journalTableNameColumn.setOnEditCommit(event -> { + if (viewModel.validateAbbreviationsProperty().get()) { + AbbreviationViewModel item = event.getRowValue(); + String newValue = event.getNewValue(); + List results = viewModel.validateAbbreviation(item.getName(), newValue, item.getAbbreviation()); + if (!results.isEmpty()) { + event.consume(); + dialogService.showErrorDialogAndWait(Localization.lang("Validation Error"), + results.stream() + .map(ValidationResult::getMessage) + .collect(Collectors.joining("\n"))); + } + } + }); journalTableAbbreviationColumn.setCellValueFactory(cellData -> cellData.getValue().abbreviationProperty()); journalTableAbbreviationColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + journalTableAbbreviationColumn.setOnEditCommit(event -> { + if (viewModel.validateAbbreviationsProperty().get()) { + AbbreviationViewModel item = event.getRowValue(); + String newValue = event.getNewValue(); + List results = viewModel.validateAbbreviation(item.getName(), item.getAbbreviation(), newValue); + if (!results.isEmpty()) { + event.consume(); + dialogService.showErrorDialogAndWait(Localization.lang("Validation Error"), + results.stream() + .map(ValidationResult::getMessage) + .collect(Collectors.joining("\n"))); + } + } + }); journalTableShortestUniqueAbbreviationColumn.setCellValueFactory(cellData -> cellData.getValue().shortestUniqueAbbreviationProperty()); journalTableShortestUniqueAbbreviationColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + journalTableShortestUniqueAbbreviationColumn.setOnEditCommit(event -> { + if (viewModel.validateAbbreviationsProperty().get()) { + AbbreviationViewModel item = event.getRowValue(); + String newValue = event.getNewValue(); + List results = viewModel.validateAbbreviation(item.getName(), item.getAbbreviation(), newValue); + if (!results.isEmpty()) { + event.consume(); + dialogService.showErrorDialogAndWait(Localization.lang("Validation Error"), + results.stream() + .map(ValidationResult::getMessage) + .collect(Collectors.joining("\n"))); + } + } + }); actionsColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); new ValueTableCellFactory() diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java index 7b95ac28244..d97a5d5c015 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java @@ -49,6 +49,7 @@ public class JournalAbbreviationsTabViewModel implements PreferenceTabViewModel private final SimpleBooleanProperty isEditableAndRemovable = new SimpleBooleanProperty(false); private final SimpleBooleanProperty isAbbreviationEditableAndRemovable = new SimpleBooleanProperty(false); private final SimpleBooleanProperty useFJournal = new SimpleBooleanProperty(true); + private final SimpleBooleanProperty validateAbbreviations = new SimpleBooleanProperty(true); private final DialogService dialogService; private final TaskExecutor taskExecutor; @@ -66,6 +67,9 @@ public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviat this.journalAbbreviationRepository = Objects.requireNonNull(journalAbbreviationRepository); this.abbreviationsPreferences = abbreviationsPreferences; + useFJournal.setValue(abbreviationsPreferences.shouldUseFJournalField()); + validateAbbreviations.setValue(abbreviationsPreferences.shouldValidateAbbreviations()); + abbreviationsCount.bind(abbreviations.sizeProperty()); currentAbbreviation.addListener((observable, oldValue, newValue) -> { boolean isAbbreviation = (newValue != null) && !newValue.isPseudoAbbreviation(); @@ -103,6 +107,10 @@ public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviat } } }); + + // Bind preferences + useFJournal.addListener((obs, oldValue, newValue) -> abbreviationsPreferences.setUseFJournalField(newValue)); + validateAbbreviations.addListener((obs, oldValue, newValue) -> abbreviationsPreferences.setValidateAbbreviations(newValue)); } @Override @@ -387,4 +395,17 @@ public SimpleBooleanProperty isFileRemovableProperty() { public SimpleBooleanProperty useFJournalProperty() { return useFJournal; } + + public BooleanProperty validateAbbreviationsProperty() { + return validateAbbreviations; + } + + public void setValidateAbbreviations(boolean validateAbbreviations) { + this.validateAbbreviations.set(validateAbbreviations); + } + + public List validateAbbreviation(String name, String abbreviation, String shortestUniqueAbbreviation) { + Abbreviation abbreviationObject = new Abbreviation(name, abbreviation, shortestUniqueAbbreviation); + return journalAbbreviationRepository.getValidationIssues(); + } } diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java new file mode 100644 index 00000000000..e4b4b59a00b --- /dev/null +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -0,0 +1,896 @@ +package org.jabref.cli; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.prefs.BackingStoreException; + +import org.jabref.logic.FilePreferences; +import org.jabref.logic.JabRefException; +import org.jabref.logic.UiCommand; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.citationkeypattern.CitationKeyGenerator; +import org.jabref.logic.exporter.AtomicFileWriter; +import org.jabref.logic.exporter.BibDatabaseWriter; +import org.jabref.logic.exporter.BibWriter; +import org.jabref.logic.exporter.BibtexDatabaseWriter; +import org.jabref.logic.exporter.EmbeddedBibFilePdfExporter; +import org.jabref.logic.exporter.Exporter; +import org.jabref.logic.exporter.ExporterFactory; +import org.jabref.logic.exporter.SelfContainedSaveConfiguration; +import org.jabref.logic.exporter.XmpPdfExporter; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.ImportException; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ImportFormatReader; +import org.jabref.logic.importer.OpenDatabase; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.SearchBasedFetcher; +import org.jabref.logic.importer.WebFetchers; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.logic.journals.JournalAbbreviationLoader; +import org.jabref.logic.journals.JournalAbbreviationRepository; +import org.jabref.logic.journals.JournalAbbreviationValidator; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.net.URLDownload; +import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.quality.consistency.BibliographyConsistencyCheck; +import org.jabref.logic.quality.consistency.BibliographyConsistencyCheckResultCsvWriter; +import org.jabref.logic.quality.consistency.BibliographyConsistencyCheckResultTxtWriter; +import org.jabref.logic.quality.consistency.BibliographyConsistencyCheckResultWriter; +import org.jabref.logic.search.DatabaseSearcher; +import org.jabref.logic.search.SearchPreferences; +import org.jabref.logic.shared.prefs.SharedDatabasePreferences; +import org.jabref.logic.util.CurrentThreadTaskExecutor; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.logic.xmp.XmpPreferences; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.search.query.SearchQuery; +import org.jabref.model.strings.StringUtil; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.model.util.FileUpdateMonitor; + +import com.airhacks.afterburner.injection.Injector; +import com.google.common.base.Throwables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ArgumentProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(ArgumentProcessor.class); + + public enum Mode { INITIAL_START, REMOTE_START } + + private final CliOptions cli; + + private final Mode startupMode; + + private final CliPreferences cliPreferences; + private final FileUpdateMonitor fileUpdateMonitor; + private final BibEntryTypesManager entryTypesManager; + + private boolean guiNeeded; + private final List uiCommands = new ArrayList<>(); + + /** + * First call the constructor, then call {@link #processArguments()}. + * Afterward, you can access the {@link #getUiCommands()}. + * + * @implNote both cli and gui preferences are passed to make the dependency to GUI parts explicit + */ + public ArgumentProcessor(String[] args, + Mode startupMode, + CliPreferences cliPreferences, + FileUpdateMonitor fileUpdateMonitor, + BibEntryTypesManager entryTypesManager) + throws org.apache.commons.cli.ParseException { + this.cli = new CliOptions(args); + this.startupMode = startupMode; + this.cliPreferences = cliPreferences; + this.fileUpdateMonitor = fileUpdateMonitor; + this.entryTypesManager = entryTypesManager; + } + + /** + * Will open a file (like {@link #importFile(String)}, but will also request JabRef to focus on this library. + * + * @return ParserResult with setToOpenTab(true) + */ + private Optional importToOpenBase(String importArguments) { + Optional result = importFile(importArguments); + result.ifPresent(ParserResult::setToOpenTab); + return result; + } + + private Optional importBibtexToOpenBase(String argument, ImportFormatPreferences importFormatPreferences) { + BibtexParser parser = new BibtexParser(importFormatPreferences); + try { + List entries = parser.parseEntries(argument); + ParserResult result = new ParserResult(entries); + result.setToOpenTab(); + return Optional.of(result); + } catch (ParseException e) { + System.err.println(Localization.lang("Error occurred when parsing entry") + ": " + e.getLocalizedMessage()); + return Optional.empty(); + } + } + + /** + * + * @param importArguments Format: fileName[,format] + */ + private Optional importFile(String importArguments) { + LOGGER.debug("Importing file {}", importArguments); + String[] data = importArguments.split(","); + + String address = data[0]; + Path file; + if (address.startsWith("http://") || address.startsWith("https://") || address.startsWith("ftp://")) { + // Download web resource to temporary file + try { + file = new URLDownload(address).toTemporaryFile(); + } catch (FetcherException | MalformedURLException e) { + System.err.println(Localization.lang("Problem downloading from %1", address) + e.getLocalizedMessage()); + return Optional.empty(); + } + } else { + if (OS.WINDOWS) { + file = Path.of(address); + } else { + file = Path.of(address.replace("~", System.getProperty("user.home"))); + } + } + + String importFormat; + if (data.length > 1) { + importFormat = data[1]; + } else { + importFormat = "*"; + } + + Optional importResult = importFile(file, importFormat); + importResult.ifPresent(result -> { + if (result.hasWarnings()) { + System.out.println(result.getErrorMessage()); + } + }); + return importResult; + } + + private Optional importFile(Path file, String importFormat) { + try { + ImportFormatReader importFormatReader = new ImportFormatReader( + cliPreferences.getImporterPreferences(), + cliPreferences.getImportFormatPreferences(), + cliPreferences.getCitationKeyPatternPreferences(), + fileUpdateMonitor + ); + + if (!"*".equals(importFormat)) { + System.out.println(Localization.lang("Importing %0", file)); + ParserResult result = importFormatReader.importFromFile(importFormat, file); + return Optional.of(result); + } else { + // * means "guess the format": + System.out.println(Localization.lang("Importing file %0 as unknown format", file)); + + ImportFormatReader.UnknownFormatImport importResult = + importFormatReader.importUnknownFormat(file, new DummyFileUpdateMonitor()); + + System.out.println(Localization.lang("Format used: %0", importResult.format())); + return Optional.of(importResult.parserResult()); + } + } catch (ImportException ex) { + System.err.println(Localization.lang("Error opening file '%0'", file) + "\n" + ex.getLocalizedMessage()); + return Optional.empty(); + } + } + + public void processArguments() { + uiCommands.clear(); + + if ((startupMode == Mode.INITIAL_START) && cli.isShowVersion()) { + cli.displayVersion(); + } + + if ((startupMode == Mode.INITIAL_START) && cli.isHelp()) { + CliOptions.printUsage(cliPreferences); + guiNeeded = false; + return; + } + + guiNeeded = true; + + // Check if we should reset all preferences to default values: + if (cli.isPreferencesReset()) { + resetPreferences(cli.getPreferencesReset()); + } + + // Check if we should import preferences from a file: + if (cli.isPreferencesImport()) { + importPreferences(); + } + + List loaded = importAndOpenFiles(); + + if (!cli.isBlank() && cli.isFetcherEngine()) { + fetch(cli.getFetcherEngine()).ifPresent(loaded::add); + } + + if (cli.isExportMatches()) { + if (!loaded.isEmpty()) { + if (!exportMatches(loaded)) { + return; + } + } else { + System.err.println(Localization.lang("The output option depends on a valid input option.")); + } + } + + if (cli.isGenerateCitationKeys()) { + regenerateCitationKeys(loaded); + } + + if ((cli.isWriteXmpToPdf() && cli.isEmbedBibFileInPdf()) || (cli.isWriteMetadataToPdf() && (cli.isWriteXmpToPdf() || cli.isEmbedBibFileInPdf()))) { + System.err.println("Give only one of [writeXmpToPdf, embedBibFileInPdf, writeMetadataToPdf]"); + } + + if (cli.isWriteMetadataToPdf() || cli.isWriteXmpToPdf() || cli.isEmbedBibFileInPdf()) { + if (!loaded.isEmpty()) { + writeMetadataToPdf(loaded, + cli.getWriteMetadataToPdf(), + cliPreferences.getXmpPreferences(), + cliPreferences.getFilePreferences(), + cliPreferences.getLibraryPreferences().getDefaultBibDatabaseMode(), + cliPreferences.getCustomEntryTypesRepository(), + cliPreferences.getFieldPreferences(), + Injector.instantiateModelOrService(JournalAbbreviationRepository.class), + cli.isWriteXmpToPdf() || cli.isWriteMetadataToPdf(), + cli.isEmbedBibFileInPdf() || cli.isWriteMetadataToPdf()); + } + } + + if (cli.isFileExport()) { + if (!loaded.isEmpty()) { + exportFile(loaded, cli.getFileExport().split(",")); + LOGGER.debug("Finished export"); + } else { + System.err.println(Localization.lang("The output option depends on a valid import option.")); + } + } + + if (cli.isPreferencesExport()) { + try { + cliPreferences.exportPreferences(Path.of(cli.getPreferencesExport())); + } catch (JabRefException ex) { + LOGGER.error("Cannot export preferences", ex); + } + } + + if (!cli.isBlank() && cli.isAuxImport()) { + doAuxImport(loaded); + } + + if (cli.isBlank()) { + uiCommands.add(new UiCommand.BlankWorkspace()); + } + + if (!cli.isBlank() && cli.isJumpToKey()) { + uiCommands.add(new UiCommand.JumpToEntryKey(cli.getJumpToKey())); + } + + if (!cli.isBlank() && !loaded.isEmpty()) { + uiCommands.add(new UiCommand.OpenDatabases(loaded)); + } + + if (cli.isBlank() && loaded.isEmpty()) { + uiCommands.add(new UiCommand.BlankWorkspace()); + } + + if (cli.isCheckConsistency()) { + if (!loaded.isEmpty()) { + checkConsistency(loaded, cli.getCheckConsistency(), cli.getCheckConsistencyOutputFormat()); + } else { + System.err.println(Localization.lang("The consistency check option depends on a valid import option.")); + } + } + + if (cli.isValidateJournals()) { + validateJournalAbbreviations(); + } + } + + private void checkConsistency(List loaded, + String fileName, + String outputFormat) { + Optional fileNameOpt = Optional.ofNullable(fileName); + Optional outputFormatOpt = Optional.ofNullable(outputFormat); + + if (fileNameOpt.isEmpty()) { + System.out.println(Localization.lang("No file specified for consistency check.")); + return; + } + + Path filePath = Path.of(fileNameOpt.get()); + ParserResult pr; + try { + pr = OpenDatabase.loadDatabase(filePath, cliPreferences.getImportFormatPreferences(), fileUpdateMonitor); + } catch (IOException ex) { + LOGGER.error("Error reading '{}'.", filePath, ex); + return; + } + BibDatabaseContext databaseContext = pr.getDatabaseContext(); + List entries = databaseContext.getDatabase().getEntries(); + + BibliographyConsistencyCheck consistencyCheck = new BibliographyConsistencyCheck(); + BibliographyConsistencyCheck.Result result = consistencyCheck.check(entries); + + Writer writer = new OutputStreamWriter(System.out); + BibliographyConsistencyCheckResultWriter checkResultWriter; + if (outputFormatOpt.isEmpty() || "txt".equalsIgnoreCase(outputFormatOpt.get())) { + checkResultWriter = new BibliographyConsistencyCheckResultTxtWriter( + result, + writer, + cli.isPorcelainOutputMode(), + entryTypesManager, + databaseContext.getMode()); + } else { + checkResultWriter = new BibliographyConsistencyCheckResultCsvWriter( + result, + writer, + cli.isPorcelainOutputMode(), + entryTypesManager, + databaseContext.getMode()); + } + + // System.out should not be closed, therefore no try-with-resources + try { + checkResultWriter.writeFindings(); + writer.flush(); + } catch (IOException e) { + LOGGER.error("Error writing results", e); + } + if (!cli.isPorcelainOutputMode()) { + System.out.println(Localization.lang("Consistency check completed")); + } + } + + private static void writeMetadataToPdf(List loaded, + String filesAndCiteKeys, + XmpPreferences xmpPreferences, + FilePreferences filePreferences, + BibDatabaseMode databaseMode, + BibEntryTypesManager entryTypesManager, + FieldPreferences fieldPreferences, + JournalAbbreviationRepository abbreviationRepository, + boolean writeXMP, + boolean embeddBibfile) { + if (loaded.isEmpty()) { + LOGGER.error("The write xmp option depends on a valid import option."); + return; + } + ParserResult pr = loaded.getLast(); + BibDatabaseContext databaseContext = pr.getDatabaseContext(); + + XmpPdfExporter xmpPdfExporter = new XmpPdfExporter(xmpPreferences); + EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter = new EmbeddedBibFilePdfExporter(databaseMode, entryTypesManager, fieldPreferences); + + if ("all".equals(filesAndCiteKeys)) { + for (BibEntry entry : databaseContext.getEntries()) { + writeMetadataToPDFsOfEntry( + databaseContext, + entry.getCitationKey().orElse(""), + entry, + filePreferences, + xmpPdfExporter, + embeddedBibFilePdfExporter, + abbreviationRepository, + writeXMP, + embeddBibfile); + } + return; + } + + List citeKeys = new ArrayList<>(); + List pdfs = new ArrayList<>(); + for (String fileOrCiteKey : filesAndCiteKeys.split(",")) { + if (fileOrCiteKey.toLowerCase(Locale.ROOT).endsWith(".pdf")) { + pdfs.add(fileOrCiteKey); + } else { + citeKeys.add(fileOrCiteKey); + } + } + + writeMetadataToPdfByCitekey( + databaseContext, + citeKeys, + filePreferences, + xmpPdfExporter, + embeddedBibFilePdfExporter, + abbreviationRepository, + writeXMP, + embeddBibfile); + writeMetadataToPdfByFileNames( + databaseContext, + pdfs, + filePreferences, + xmpPdfExporter, + embeddedBibFilePdfExporter, + abbreviationRepository, + writeXMP, + embeddBibfile); + } + + private static void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext, + String citeKey, + BibEntry entry, + FilePreferences filePreferences, + XmpPdfExporter xmpPdfExporter, + EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, + JournalAbbreviationRepository abbreviationRepository, + boolean writeXMP, + boolean embedBibfile) { + try { + if (writeXMP) { + if (xmpPdfExporter.exportToAllFilesOfEntry(databaseContext, filePreferences, entry, List.of(entry), abbreviationRepository)) { + System.out.printf("Successfully written XMP metadata on at least one linked file of %s%n", citeKey); + } else { + System.err.printf("Cannot write XMP metadata on any linked files of %s. Make sure there is at least one linked file and the path is correct.%n", citeKey); + } + } + if (embedBibfile) { + if (embeddedBibFilePdfExporter.exportToAllFilesOfEntry(databaseContext, filePreferences, entry, List.of(entry), abbreviationRepository)) { + System.out.printf("Successfully embedded metadata on at least one linked file of %s%n", citeKey); + } else { + System.out.printf("Cannot embed metadata on any linked files of %s. Make sure there is at least one linked file and the path is correct.%n", citeKey); + } + } + } catch (Exception e) { + LOGGER.error("Failed writing metadata on a linked file of {}.", citeKey); + } + } + + private static void writeMetadataToPdfByCitekey(BibDatabaseContext databaseContext, + List citeKeys, + FilePreferences filePreferences, + XmpPdfExporter xmpPdfExporter, + EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, + JournalAbbreviationRepository abbreviationRepository, + boolean writeXMP, + boolean embeddBibfile) { + for (String citeKey : citeKeys) { + List bibEntryList = databaseContext.getDatabase().getEntriesByCitationKey(citeKey); + if (bibEntryList.isEmpty()) { + System.err.printf("Skipped - Cannot find %s in library.%n", citeKey); + continue; + } + for (BibEntry entry : bibEntryList) { + writeMetadataToPDFsOfEntry(databaseContext, citeKey, entry, filePreferences, xmpPdfExporter, embeddedBibFilePdfExporter, abbreviationRepository, writeXMP, embeddBibfile); + } + } + } + + private static void writeMetadataToPdfByFileNames(BibDatabaseContext databaseContext, + List pdfs, + FilePreferences filePreferences, + XmpPdfExporter xmpPdfExporter, + EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, + JournalAbbreviationRepository abbreviationRepository, + boolean writeXMP, + boolean embeddBibfile) { + for (String fileName : pdfs) { + Path filePath = Path.of(fileName); + if (!filePath.isAbsolute()) { + filePath = FileUtil.find(fileName, databaseContext.getFileDirectories(filePreferences)).orElse(FileUtil.find(fileName, List.of(Path.of("").toAbsolutePath())).orElse(filePath)); + } + if (Files.exists(filePath)) { + try { + if (writeXMP) { + if (xmpPdfExporter.exportToFileByPath(databaseContext, filePreferences, filePath, abbreviationRepository)) { + System.out.printf("Successfully written XMP metadata of at least one entry to %s%n", fileName); + } else { + System.out.printf("File %s is not linked to any entry in database.%n", fileName); + } + } + if (embeddBibfile) { + if (embeddedBibFilePdfExporter.exportToFileByPath(databaseContext, filePreferences, filePath, abbreviationRepository)) { + System.out.printf("Successfully embedded XMP metadata of at least one entry to %s%n", fileName); + } else { + System.out.printf("File %s is not linked to any entry in database.%n", fileName); + } + } + } catch (IOException e) { + LOGGER.error("Error accessing file '{}'.", fileName); + } catch (Exception e) { + LOGGER.error("Error writing entry to {}.", fileName); + } + } else { + LOGGER.error("Skipped - PDF {} does not exist", fileName); + } + } + } + + private boolean exportMatches(List loaded) { + String[] data = cli.getExportMatches().split(","); + String searchTerm = data[0].replace("\\$", " "); // enables blanks within the search term: + // $ stands for a blank + ParserResult pr = loaded.getLast(); + BibDatabaseContext databaseContext = pr.getDatabaseContext(); + + SearchPreferences searchPreferences = cliPreferences.getSearchPreferences(); + SearchQuery query = new SearchQuery(searchTerm, searchPreferences.getSearchFlags()); + + List matches; + try { + // extract current thread task executor from indexManager + matches = new DatabaseSearcher(query, databaseContext, new CurrentThreadTaskExecutor(), cliPreferences).getMatches(); + } catch (IOException e) { + LOGGER.error("Error occurred when searching", e); + return false; + } + + // export matches + if (!matches.isEmpty()) { + String formatName; + + // read in the export format, take default format if no format entered + switch (data.length) { + case 3 -> formatName = data[2]; + case 2 -> + // default exporter: bib file + formatName = "bib"; + default -> { + System.err.println(Localization.lang("Output file missing").concat(". \n \t ") + .concat(Localization.lang("Usage")).concat(": ") + CliOptions.getExportMatchesSyntax()); + guiNeeded = false; + return false; + } + } + + if ("bib".equals(formatName)) { + // output a bib file as default or if + // provided exportFormat is "bib" + saveDatabase(new BibDatabase(matches), data[1]); + LOGGER.debug("Finished export"); + } else { + // export new database + ExporterFactory exporterFactory = ExporterFactory.create(cliPreferences); + Optional exporter = exporterFactory.getExporterByName(formatName); + if (exporter.isEmpty()) { + System.err.println(Localization.lang("Unknown export format %0", formatName)); + } else { + // We have an TemplateExporter instance: + try { + System.out.println(Localization.lang("Exporting %0", data[1])); + exporter.get().export( + databaseContext, + Path.of(data[1]), + matches, + Collections.emptyList(), + Injector.instantiateModelOrService(JournalAbbreviationRepository.class)); + } catch (Exception ex) { + System.err.println(Localization.lang("Could not export file '%0' (reason: %1)", data[1], Throwables.getStackTraceAsString(ex))); + } + } + } + } else { + System.err.println(Localization.lang("No search matches.")); + } + return true; + } + + private void doAuxImport(List loaded) { + boolean usageMsg; + + if (!loaded.isEmpty()) { + usageMsg = generateAux(loaded, cli.getAuxImport().split(",")); + } else { + usageMsg = true; + } + + if (usageMsg) { + System.out.println(Localization.lang("no base-BibTeX-file specified!")); + System.out.println(Localization.lang("usage") + " :"); + System.out.println("jabref --aux infile[.aux],outfile[.bib] base-BibTeX-file"); + } + } + + /** + * @return List of opened files (could be .bib, but also other formats). May also contain error results. + */ + private List importAndOpenFiles() { + List loaded = new ArrayList<>(); + List toImport = new ArrayList<>(); + if (!cli.isBlank() && (!cli.getLeftOver().isEmpty())) { + for (String aLeftOver : cli.getLeftOver()) { + // Leftover arguments that have a "bib" extension are interpreted as + // BIB files to open. Other files, and files that could not be opened + // as bib, we try to import instead. + boolean bibExtension = aLeftOver.toLowerCase(Locale.ENGLISH).endsWith("bib"); + + ParserResult pr = new ParserResult(); + if (bibExtension) { + try { + pr = OpenDatabase.loadDatabase( + Path.of(aLeftOver), + cliPreferences.getImportFormatPreferences(), + fileUpdateMonitor); + // In contrast to org.jabref.gui.LibraryTab.onDatabaseLoadingSucceed, we do not execute OpenDatabaseAction.performPostOpenActions(result, dialogService); + } catch (IOException ex) { + pr = ParserResult.fromError(ex); + LOGGER.error("Error opening file '{}'", aLeftOver, ex); + } + } + + if (!bibExtension || (pr.isEmpty())) { + // We will try to import this file. Normally we + // will import it into a new tab, but if this import has + // been initiated by another instance through the remote + // listener, we will instead import it into the current library. + // This will enable easy integration with web browsers that can + // open a reference file in JabRef. + if (startupMode == Mode.INITIAL_START) { + toImport.add(aLeftOver); + } else { + loaded.add(importToOpenBase(aLeftOver).orElse(new ParserResult())); + } + } else { + loaded.add(pr); + } + } + } + + if (!cli.isBlank() && cli.isFileImport()) { + toImport.add(cli.getFileImport()); + } + + for (String filenameString : toImport) { + importFile(filenameString).ifPresent(loaded::add); + } + + if (!cli.isBlank() && cli.isImportToOpenBase()) { + importToOpenBase(cli.getImportToOpenBase()).ifPresent(loaded::add); + } + + if (!cli.isBlank() && cli.isBibtexImport()) { + importBibtexToOpenBase(cli.getBibtexImport(), cliPreferences.getImportFormatPreferences()).ifPresent(loaded::add); + } + + return loaded; + } + + private boolean generateAux(List loaded, String[] data) { + if (data.length == 2) { + ParserResult pr = loaded.getFirst(); + AuxCommandLine acl = new AuxCommandLine(data[0], pr.getDatabase()); + BibDatabase newBase = acl.perform(); + + boolean notSavedMsg = false; + + // write an output, if something could be resolved + if ((newBase != null) && newBase.hasEntries()) { + String subName = StringUtil.getCorrectFileName(data[1], "bib"); + saveDatabase(newBase, subName); + notSavedMsg = true; + } + + if (!notSavedMsg) { + System.out.println(Localization.lang("no library generated")); + } + return false; + } else { + return true; + } + } + + private void saveDatabase(BibDatabase newBase, String subName) { + try { + System.out.println(Localization.lang("Saving") + ": " + subName); + try (AtomicFileWriter fileWriter = new AtomicFileWriter(Path.of(subName), StandardCharsets.UTF_8)) { + BibWriter bibWriter = new BibWriter(fileWriter, OS.NEWLINE); + SelfContainedSaveConfiguration saveConfiguration = (SelfContainedSaveConfiguration) new SelfContainedSaveConfiguration() + .withReformatOnSave(cliPreferences.getLibraryPreferences().shouldAlwaysReformatOnSave()); + BibDatabaseWriter databaseWriter = new BibtexDatabaseWriter( + bibWriter, + saveConfiguration, + cliPreferences.getFieldPreferences(), + cliPreferences.getCitationKeyPatternPreferences(), + entryTypesManager); + databaseWriter.saveDatabase(new BibDatabaseContext(newBase)); + + // Show just a warning message if encoding did not work for all characters: + if (fileWriter.hasEncodingProblems()) { + System.err.println(Localization.lang("Warning") + ": " + + Localization.lang("UTF-8 could not be used to encode the following characters: %0", fileWriter.getEncodingProblems())); + } + } + } catch (IOException ex) { + System.err.println(Localization.lang("Could not save file.") + "\n" + ex.getLocalizedMessage()); + } + } + + private void exportFile(List loaded, String[] data) { + if (data.length == 1) { + // This signals that the latest import should be stored in BibTeX + // format to the given file. + if (!loaded.isEmpty()) { + ParserResult pr = loaded.getLast(); + if (!pr.isInvalid()) { + saveDatabase(pr.getDatabase(), data[0]); + } + } else { + System.err.println(Localization.lang("The output option depends on a valid import option.")); + } + } else if (data.length == 2) { + // This signals that the latest import should be stored in the given + // format to the given file. + ParserResult parserResult = loaded.getLast(); + + Path path = parserResult.getPath().get().toAbsolutePath(); + BibDatabaseContext databaseContext = parserResult.getDatabaseContext(); + databaseContext.setDatabasePath(path); + List fileDirForDatabase = databaseContext + .getFileDirectories(cliPreferences.getFilePreferences()); + System.out.println(Localization.lang("Exporting %0", data[0])); + ExporterFactory exporterFactory = ExporterFactory.create(cliPreferences); + Optional exporter = exporterFactory.getExporterByName(data[1]); + if (exporter.isEmpty()) { + System.err.println(Localization.lang("Unknown export format %0", data[1])); + } else { + // We have an exporter: + try { + exporter.get().export( + parserResult.getDatabaseContext(), + Path.of(data[0]), + parserResult.getDatabaseContext().getDatabase().getEntries(), + fileDirForDatabase, + Injector.instantiateModelOrService(JournalAbbreviationRepository.class)); + } catch (Exception ex) { + System.err.println(Localization.lang("Could not export file '%0' (reason: %1)", data[0], Throwables.getStackTraceAsString(ex))); + } + } + } + } + + private void importPreferences() { + try { + cliPreferences.importPreferences(Path.of(cli.getPreferencesImport())); + Injector.setModelOrService(BibEntryTypesManager.class, cliPreferences.getCustomEntryTypesRepository()); + } catch (JabRefException ex) { + LOGGER.error("Cannot import preferences", ex); + } + } + + private void resetPreferences(String value) { + if ("all".equals(value.trim())) { + try { + System.out.println(Localization.lang("Setting all preferences to default values.")); + cliPreferences.clear(); + new SharedDatabasePreferences().clear(); + } catch (BackingStoreException e) { + System.err.println(Localization.lang("Unable to clear preferences.")); + LOGGER.error("Unable to clear preferences", e); + } + } else { + String[] keys = value.split(","); + for (String key : keys) { + try { + cliPreferences.deleteKey(key.trim()); + System.out.println(Localization.lang("Resetting preference key '%0'", key.trim())); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + } + } + + private void regenerateCitationKeys(List loaded) { + for (ParserResult parserResult : loaded) { + BibDatabase database = parserResult.getDatabase(); + + LOGGER.info(Localization.lang("Regenerating citation keys according to metadata")); + + CitationKeyGenerator keyGenerator = new CitationKeyGenerator( + parserResult.getDatabaseContext(), + cliPreferences.getCitationKeyPatternPreferences()); + for (BibEntry entry : database.getEntries()) { + keyGenerator.generateAndSetKey(entry); + } + } + } + + /** + * Run an entry fetcher from the command line. + * + * @param fetchCommand A string containing both the name of the fetcher to use and the search query, separated by a : + * @return A parser result containing the entries fetched or null if an error occurred. + */ + private Optional fetch(String fetchCommand) { + if ((fetchCommand == null) || !fetchCommand.contains(":")) { + System.out.println(Localization.lang("Expected syntax for --fetch=':'")); + System.out.println(Localization.lang("The following fetchers are available:")); + return Optional.empty(); + } + + String[] split = fetchCommand.split(":"); + String engine = split[0]; + String query = split[1]; + + Set fetchers = WebFetchers.getSearchBasedFetchers( + cliPreferences.getImportFormatPreferences(), + cliPreferences.getImporterPreferences()); + Optional selectedFetcher = fetchers.stream() + .filter(fetcher -> fetcher.getName().equalsIgnoreCase(engine)) + .findFirst(); + if (selectedFetcher.isEmpty()) { + System.out.println(Localization.lang("Could not find fetcher '%0'", engine)); + + System.out.println(Localization.lang("The following fetchers are available:")); + fetchers.forEach(fetcher -> System.out.println(" " + fetcher.getName())); + + return Optional.empty(); + } else { + System.out.println(Localization.lang("Running query '%0' with fetcher '%1'.", query, engine)); + System.out.print(Localization.lang("Please wait...")); + try { + List matches = selectedFetcher.get().performSearch(query); + if (matches.isEmpty()) { + System.out.println("\r" + Localization.lang("No results found.")); + return Optional.empty(); + } else { + System.out.println("\r" + Localization.lang("Found %0 results.", String.valueOf(matches.size()))); + return Optional.of(new ParserResult(matches)); + } + } catch (FetcherException e) { + LOGGER.error("Error while fetching", e); + return Optional.empty(); + } + } + } + + public boolean shouldShutDown() { + return cli.isDisableGui() || cli.isShowVersion() || !guiNeeded; + } + + public List getUiCommands() { + return uiCommands; + } + + private void validateJournalAbbreviations() { + JournalAbbreviationRepository repository = loadJournalAbbreviationRepository(); + List issues = repository.getValidationIssues(); + printValidationResults(issues); + } + + private JournalAbbreviationRepository loadJournalAbbreviationRepository() { + return JournalAbbreviationLoader.loadRepository(cliPreferences.getJournalAbbreviationPreferences()); + } + + private void printValidationResults(List issues) { + if (issues.isEmpty()) { + System.out.println(Localization.lang("No validation issues found in journal abbreviations")); + } else { + System.out.println(Localization.lang("Found %0 validation issues", issues.size())); + for (JournalAbbreviationValidator.ValidationResult issue : issues) { + System.out.println(Localization.lang("Type: %0, Message: %1", + issue.getType().name(), + issue.getMessage())); + } + } + } +} diff --git a/src/main/java/org/jabref/cli/CliOptions.java b/src/main/java/org/jabref/cli/CliOptions.java new file mode 100644 index 00000000000..9c380749238 --- /dev/null +++ b/src/main/java/org/jabref/cli/CliOptions.java @@ -0,0 +1,420 @@ +package org.jabref.cli; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javafx.util.Pair; + +import org.jabref.logic.exporter.ExporterFactory; +import org.jabref.logic.importer.ImportFormatReader; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.util.BuildInfo; +import org.jabref.model.strings.StringUtil; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** + * Holds the command line options. It parses it using Apache Commons CLI. + */ +public class CliOptions { + private static final int WIDTH = 100; // Number of characters per line before a line break must be added. + private static final String WRAPPED_LINE_PREFIX = ""; // If a line break is added, this prefix will be inserted at the beginning of the next line + private static final String STRING_TABLE_DELIMITER = " : "; + + private final CommandLine commandLine; + private final List leftOver; + + public CliOptions(String[] args) throws ParseException { + Options options = getOptions(); + this.commandLine = new DefaultParser().parse(options, args, true); + this.leftOver = commandLine.getArgList(); + } + + public static String getExportMatchesSyntax() { + return "[%s]searchTerm,outputFile:%s[,%s]".formatted( + Localization.lang("field"), + Localization.lang("file"), + Localization.lang("exportFormat")); + } + + public boolean isHelp() { + return commandLine.hasOption("help"); + } + + public boolean isShowVersion() { + return commandLine.hasOption("version"); + } + + public boolean isBlank() { + return commandLine.hasOption("blank"); + } + + public boolean isDisableGui() { + return commandLine.hasOption("nogui"); + } + + public boolean isCheckConsistency() { + return commandLine.hasOption("check-consistency"); + } + + public String getCheckConsistency() { + return commandLine.getOptionValue("check-consistency"); + } + + public String getCheckConsistencyOutputFormat() { + return commandLine.getOptionValue("output-format"); + } + + public boolean isPorcelainOutputMode() { + return commandLine.hasOption("porcelain"); + } + + public boolean isPreferencesExport() { + return commandLine.hasOption("prexp"); + } + + public String getPreferencesExport() { + return commandLine.getOptionValue("prexp", "jabref_prefs.xml"); + } + + public boolean isPreferencesImport() { + return commandLine.hasOption("primp"); + } + + public String getPreferencesImport() { + return commandLine.getOptionValue("primp", "jabref_prefs.xml"); + } + + public boolean isPreferencesReset() { + return commandLine.hasOption("prdef"); + } + + public String getPreferencesReset() { + return commandLine.getOptionValue("prdef"); + } + + public boolean isFileExport() { + return commandLine.hasOption("output"); + } + + public String getFileExport() { + return commandLine.getOptionValue("output"); + } + + public boolean isBibtexImport() { + return commandLine.hasOption("importBibtex"); + } + + public String getBibtexImport() { + return commandLine.getOptionValue("importBibtex"); + } + + public boolean isFileImport() { + return commandLine.hasOption("import"); + } + + public String getFileImport() { + return commandLine.getOptionValue("import"); + } + + public boolean isAuxImport() { + return commandLine.hasOption("aux"); + } + + public String getAuxImport() { + return commandLine.getOptionValue("aux"); + } + + public boolean isImportToOpenBase() { + return commandLine.hasOption("importToOpen"); + } + + public String getImportToOpenBase() { + return commandLine.getOptionValue("importToOpen"); + } + + public boolean isDebugLogging() { + return commandLine.hasOption("debug"); + } + + public boolean isFetcherEngine() { + return commandLine.hasOption("fetch"); + } + + public String getFetcherEngine() { + return commandLine.getOptionValue("fetch"); + } + + public boolean isExportMatches() { + return commandLine.hasOption("exportMatches"); + } + + public String getExportMatches() { + return commandLine.getOptionValue("exportMatches"); + } + + public boolean isGenerateCitationKeys() { + return commandLine.hasOption("generateCitationKeys"); + } + + public boolean isWriteXmpToPdf() { + return commandLine.hasOption("writeXmpToPdf"); + } + + public boolean isEmbedBibFileInPdf() { + return commandLine.hasOption("embedBibFileInPdf"); + } + + public boolean isWriteMetadataToPdf() { + return commandLine.hasOption("writeMetadataToPdf"); + } + + public String getWriteMetadataToPdf() { + return commandLine.hasOption("writeMetadatatoPdf") ? commandLine.getOptionValue("writeMetadataToPdf") : + commandLine.hasOption("writeXMPtoPdf") ? commandLine.getOptionValue("writeXmpToPdf") : + commandLine.hasOption("embeddBibfileInPdf") ? commandLine.getOptionValue("embeddBibfileInPdf") : null; + } + + public String getJumpToKey() { + return commandLine.getOptionValue("jumpToKey"); + } + + public boolean isJumpToKey() { + return commandLine.hasOption("jumpToKey"); + } + + public boolean isValidateJournals() { + return commandLine.hasOption("validate-journals"); + } + + public Optional getValidateJournals() { + return Optional.ofNullable(commandLine.getOptionValue("validate-journals")); + } + + private static Options getOptions() { + Options options = new Options(); + + // boolean options + options.addOption("h", "help", false, Localization.lang("Display help on command line options")); + options.addOption("n", "nogui", false, Localization.lang("No GUI. Only process command line options")); + options.addOption("g", "generateCitationKeys", false, Localization.lang("Regenerate all keys for the entries in a BibTeX file")); + options.addOption("b", "blank", false, Localization.lang("Do not open any files at startup")); + options.addOption("v", "version", false, Localization.lang("Display version")); + options.addOption(null, "debug", false, Localization.lang("Show debug level messages")); + + options.addOption(Option + .builder("i") + .longOpt("import") + .desc("%s: '%s'".formatted(Localization.lang("Import file"), "-i library.bib")) + .hasArg() + .argName("FILE[,FORMAT]") + .build()); + + options.addOption(Option + .builder() + .longOpt("importToOpen") + .desc(Localization.lang("Same as --import, but will be imported to the opened tab")) + .hasArg() + .argName("FILE[,FORMAT]") + .build()); + + options.addOption(Option + .builder("ib") + .longOpt("importBibtex") + .desc("%s: '%s'".formatted(Localization.lang("Import BibTeX"), "-ib @article{entry}")) + .hasArg() + .argName("BIBTEX_STRING") + .build()); + + options.addOption(Option + .builder("o") + .longOpt("output") + .desc("%s: '%s'".formatted(Localization.lang("Export an input to a file"), "-i db.bib -o db.htm,html")) + .hasArg() + .argName("FILE[,FORMAT]") + .build()); + + options.addOption(Option + .builder("m") + .longOpt("exportMatches") + .desc("%s: '%s'".formatted(Localization.lang("Matching"), "-i db.bib -m author=Newton,search.htm,html")) + .hasArg() + .argName("QUERY,FILE[,FORMAT]") + .build()); + + options.addOption(Option + .builder("f") + .longOpt("fetch") + .desc("%s: '%s'".formatted(Localization.lang("Run fetcher"), "-f Medline/PubMed:cancer")) + .hasArg() + .argName("FETCHER:QUERY") + .build()); + + options.addOption(Option + .builder("a") + .longOpt("aux") + .desc("%s: '%s'".formatted(Localization.lang("Sublibrary from AUX to BibTeX"), "-a thesis.aux,new.bib")) + .hasArg() + .argName("FILE[.aux],FILE[.bib] FILE") + .build()); + + options.addOption(Option + .builder("x") + .longOpt("prexp") + .desc("%s: '%s'".formatted(Localization.lang("Export preferences to a file"), "-x prefs.xml")) + .hasArg() + .argName("[FILE]") + .build()); + + options.addOption(Option + .builder("p") + .longOpt("primp") + .desc("%s: '%s'".formatted(Localization.lang("Import preferences from a file"), "-p prefs.xml")) + .hasArg() + .argName("[FILE]") + .build()); + + options.addOption(Option + .builder("d") + .longOpt("prdef") + .desc("%s: '%s'".formatted(Localization.lang("Reset preferences"), "-d mainFontSize,newline' or '-d all")) + .hasArg() + .argName("KEY1[,KEY2][,KEYn] | all") + .build()); + + options.addOption(Option + .builder() + .longOpt("writeXmpToPdf") + .desc("%s: '%s'".formatted(Localization.lang("Write BibTeX as XMP metadata to PDF."), "-w pathToMyOwnPaper.pdf")) + .hasArg() + .argName("CITEKEY1[,CITEKEY2][,CITEKEYn] | PDF1[,PDF2][,PDFn] | all") + .build()); + + options.addOption(Option + .builder() + .longOpt("embedBibFileInPdf") + .desc("%s: '%s'".formatted(Localization.lang("Embed BibTeX as attached file in PDF."), "-w pathToMyOwnPaper.pdf")) + .hasArg() + .argName("CITEKEY1[,CITEKEY2][,CITEKEYn] | PDF1[,PDF2][,PDFn] | all") + .build()); + + options.addOption(Option + .builder("w") + .longOpt("writeMetadataToPdf") + .desc("%s: '%s'".formatted(Localization.lang("Write BibTeX to PDF (XMP and embedded)"), "-w pathToMyOwnPaper.pdf")) + .hasArg() + .argName("CITEKEY1[,CITEKEY2][,CITEKEYn] | PDF1[,PDF2][,PDFn] | all") + .build()); + + options.addOption(Option + .builder("j") + .longOpt("jumpToKey") + .desc("%s: '%s'".formatted(Localization.lang("Jump to the entry of the given citation key."), "-j key")) + .hasArg() + .argName("CITATIONKEY") + .build()); + + options.addOption(Option + .builder("cc") + .longOpt("check-consistency") + .desc(Localization.lang("Check consistency of BibTeX file")) + .hasArg() + .argName("FILE") + .build()); + + options.addOption(Option + .builder() + .longOpt("output-format") + .desc(Localization.lang("Output format for consistency check (txt/csv)")) + .hasArg() + .argName("FORMAT") + .build()); + + options.addOption(Option + .builder("porcelain") + .longOpt("porcelain") + .desc(Localization.lang("Script-friendly output")) + .build()); + + options.addOption(Option + .builder() + .longOpt("validate-journals") + .desc(Localization.lang("Validate journal abbreviation files")) + .build()); + + return options; + } + + public void displayVersion() { + System.out.println(getVersionInfo()); + } + + public static void printUsage(CliPreferences preferences) { + String header = ""; + + ImportFormatReader importFormatReader = new ImportFormatReader( + preferences.getImporterPreferences(), + preferences.getImportFormatPreferences(), + preferences.getCitationKeyPatternPreferences(), + new DummyFileUpdateMonitor() + ); + List> importFormats = importFormatReader + .getImportFormats().stream() + .map(format -> new Pair<>(format.getName(), format.getId())) + .toList(); + String importFormatsIntro = Localization.lang("Available import formats"); + String importFormatsList = "%s:%n%s%n".formatted(importFormatsIntro, alignStringTable(importFormats)); + + ExporterFactory exporterFactory = ExporterFactory.create(preferences); + List> exportFormats = exporterFactory + .getExporters().stream() + .map(format -> new Pair<>(format.getName(), format.getId())) + .toList(); + String outFormatsIntro = Localization.lang("Available export formats"); + String outFormatsList = "%s:%n%s%n".formatted(outFormatsIntro, alignStringTable(exportFormats)); + + String footer = '\n' + importFormatsList + outFormatsList + "\nPlease report issues at https://github.com/JabRef/jabref/issues."; + + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(WIDTH, "jabref [OPTIONS] [BIBTEX_FILE]\n\nOptions:", header, getOptions(), footer, true); + } + + private String getVersionInfo() { + return "JabRef %s".formatted(new BuildInfo().version); + } + + public List getLeftOver() { + return leftOver; + } + + protected static String alignStringTable(List> table) { + StringBuilder sb = new StringBuilder(); + + int maxLength = table.stream() + .mapToInt(pair -> Objects.requireNonNullElse(pair.getKey(), "").length()) + .max().orElse(0); + + for (Pair pair : table) { + int padding = Math.max(0, maxLength - pair.getKey().length()); + sb.append(WRAPPED_LINE_PREFIX); + sb.append(pair.getKey()); + + sb.append(StringUtil.repeatSpaces(padding)); + + sb.append(STRING_TABLE_DELIMITER); + sb.append(pair.getValue()); + sb.append(OS.NEWLINE); + } + + return sb.toString(); + } +} diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationValidator.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationValidator.java new file mode 100644 index 00000000000..93de3044291 --- /dev/null +++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationValidator.java @@ -0,0 +1,343 @@ +package org.jabref.logic.journals; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.tuple.Pair; + +public class JournalAbbreviationValidator { + + public static class ValidationResult { + private final boolean isValid; + private final String message; + private final ValidationType type; + private final String fullName; + private final String abbreviation; + private final int lineNumber; + private final String suggestion; + + public ValidationResult(boolean isValid, String message, ValidationType type, + String fullName, String abbreviation, int lineNumber) { + this(isValid, message, type, fullName, abbreviation, lineNumber, ""); + } + + public ValidationResult(boolean isValid, String message, ValidationType type, + String fullName, String abbreviation, int lineNumber, String suggestion) { + this.isValid = isValid; + this.message = message; + this.type = type; + this.fullName = fullName; + this.abbreviation = abbreviation; + this.lineNumber = lineNumber; + this.suggestion = suggestion; + } + + public boolean isValid() { + return isValid; + } + + public String getMessage() { + return message; + } + + public ValidationType getType() { + return type; + } + + public String getFullName() { + return fullName; + } + + public String getAbbreviation() { + return abbreviation; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getSuggestion() { + return suggestion; + } + + @Override + public String toString() { + return type + " at line " + lineNumber + ": " + message + + (suggestion.isEmpty() ? "" : " Suggestion: " + suggestion) + + " [" + fullName + " -> " + abbreviation + "]"; + } + } + + private static final Set> ALLOWED_MISMATCHES = Set.of( + Pair.of("Polish Academy of Sciences", "Acta Phys. Polon."), + Pair.of("Jagellonian University", "Acta Phys. Polon."), + Pair.of("Universităţii din Timișoara", "An. Univ."), + Pair.of("Universităţii \"Ovidius\" Constanţa", "An. Ştiinţ.") + ); + + public enum ValidationType { + ERROR, + WARNING + } + + // Updated pattern to include more valid escape sequences + private static final Pattern INVALID_ESCAPE_PATTERN = Pattern.compile("(? issues = new ArrayList<>(); + private final Map> fullNameToAbbrev = new HashMap<>(); + private final Map> abbrevToFullName = new HashMap<>(); + + /** + * Returns the list of validation issues found during validation + */ + public List getIssues() { + return new ArrayList<>(issues); + } + + /** + * Checks if the journal name or abbreviation contains wrong escape characters + */ + public Optional checkWrongEscape(String fullName, String abbreviation, int lineNumber) { + if (fullName == null || abbreviation == null) { + return Optional.empty(); + } + + List escapeIssues = new ArrayList<>(); + + // Check full name + Matcher fullNameMatcher = INVALID_ESCAPE_PATTERN.matcher(fullName); + if (fullNameMatcher.find()) { + escapeIssues.add(new ValidationResult(false, + String.format("Invalid escape sequence in full name at position %d", fullNameMatcher.start()), + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber, + "Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX")); + } + + // Check abbreviation + Matcher abbrevMatcher = INVALID_ESCAPE_PATTERN.matcher(abbreviation); + if (abbrevMatcher.find()) { + escapeIssues.add(new ValidationResult(false, + String.format("Invalid escape sequence in abbreviation at position %d", abbrevMatcher.start()), + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber, + "Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX")); + } + + return escapeIssues.isEmpty() ? + Optional.of(new ValidationResult(true, "", ValidationType.ERROR, fullName, abbreviation, lineNumber)) : + Optional.of(escapeIssues.get(0)); + } + + /** + * Checks if the journal name or abbreviation contains non-UTF8 characters + */ + public Optional checkNonUtf8(String fullName, String abbreviation, int lineNumber) { + if (fullName == null || abbreviation == null) { + return Optional.empty(); + } + + if (!isValidUtf8(fullName) || !isValidUtf8(abbreviation)) { + return Optional.of(new ValidationResult(false, + "Journal name or abbreviation contains invalid UTF-8 sequences", + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber, + "Ensure all characters are valid UTF-8. Remove or replace any invalid characters.")); + } + return Optional.of(new ValidationResult(true, "", ValidationType.ERROR, fullName, abbreviation, lineNumber)); + } + + private boolean isValidUtf8(String str) { + try { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return new String(bytes, StandardCharsets.UTF_8).equals(str); + } catch (Exception e) { + return false; + } + } + + /** + * Checks if the abbreviation starts with the same letter as the full name + */ + public Optional checkStartingLetters(String fullName, String abbreviation, int lineNumber) { + if (fullName == null || abbreviation == null) { + return Optional.empty(); + } + + fullName = fullName.trim(); + abbreviation = abbreviation.trim(); + + if (isAllowedException(fullName, abbreviation)) { + return Optional.of(new ValidationResult( + true, + "Allowed abbreviation exception", + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber + )); + } + + String fullFirst = getFirstSignificantWord(fullName); + String abbrFirst = getFirstSignificantWord(abbreviation); + + // Validate initials + if (!abbrFirst.isEmpty() && + !fullFirst.isEmpty() && + !abbrFirst.toLowerCase().startsWith(fullFirst.substring(0, 1).toLowerCase())) { + return Optional.of(new ValidationResult( + false, + "Abbreviation does not begin with same letter as full journal name", + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber, + String.format("Should start with '%c' (from '%s')", + fullFirst.toLowerCase().charAt(0), + fullFirst) + )); + } + + return Optional.of(new ValidationResult( + true, + "", + ValidationType.ERROR, + fullName, + abbreviation, + lineNumber + )); + } + + private boolean isAllowedException(String fullName, String abbreviation) { + return ALLOWED_MISMATCHES.stream() + .anyMatch(pair -> + fullName.equals(pair.getKey()) && + abbreviation.startsWith(pair.getValue()) + ); + } + + private String getFirstSignificantWord(String s) { + if (s == null || s.trim().isEmpty()) { + return ""; + } + return s.trim().split("[\\s\\-–.,:;'\"]+")[0]; + } + + /** + * Checks if the abbreviation is the same as the full text + */ + public Optional checkAbbreviationEqualsFullText(String fullName, String abbreviation, int lineNumber) { + if (fullName == null || abbreviation == null) { + return Optional.empty(); + } + + if (fullName.equalsIgnoreCase(abbreviation) && fullName.trim().split("\\s+").length > 1) { + return Optional.of(new ValidationResult(false, + "Abbreviation is the same as the full text", + ValidationType.WARNING, + fullName, + abbreviation, + lineNumber, + "Consider using a shorter abbreviation to distinguish it from the full name")); + } + + return Optional.of(new ValidationResult(true, "", ValidationType.ERROR, fullName, abbreviation, lineNumber)); + } + + /** + * Checks if the abbreviation uses outdated "Manage." instead of "Manag." + */ + public Optional checkOutdatedManagementAbbreviation(String fullName, String abbreviation, int lineNumber) { + if (fullName == null || abbreviation == null) { + return Optional.empty(); + } + + if (fullName.contains("Management") && abbreviation.contains("Manage.")) { + return Optional.of(new ValidationResult(false, + "Management is abbreviated with outdated \"Manage.\" instead of \"Manag.\"", + ValidationType.WARNING, + fullName, + abbreviation, + lineNumber, + "Update to use the standard abbreviation \"Manag.\"")); + } + return Optional.of(new ValidationResult(true, "", ValidationType.WARNING, fullName, abbreviation, lineNumber)); + } + + /** + * Check for duplicate full names with different abbreviations + */ + private void checkDuplicateFullNames() { + for (Map.Entry> entry : fullNameToAbbrev.entrySet()) { + if (entry.getValue().size() > 1) { + String fullName = entry.getKey(); + List abbreviations = entry.getValue(); + + issues.add(new ValidationResult(false, + String.format("Duplicate full name '%s' with different abbreviations: %s", + fullName, String.join(", ", abbreviations)), + ValidationType.WARNING, + fullName, + abbreviations.get(0), + -1, + "Consider consolidating abbreviations or using more specific full names")); + } + } + } + + /** + * Check for duplicate abbreviations with different full names + */ + private void checkDuplicateAbbreviations() { + for (Map.Entry> entry : abbrevToFullName.entrySet()) { + if (entry.getValue().size() > 1) { + String abbreviation = entry.getKey(); + List fullNames = entry.getValue(); + + issues.add(new ValidationResult(false, + String.format("Duplicate abbreviation '%s' used for different journals: %s", + abbreviation, String.join("; ", fullNames)), + ValidationType.WARNING, + fullNames.get(0), + abbreviation, + -1, + "Consider using more specific abbreviations to avoid ambiguity")); + } + } + } + + /** + * Validates a journal entry against all rules + */ + public List validate(String fullName, String abbreviation, int lineNumber) { + List results = new ArrayList<>(); + + // Error checks + checkWrongEscape(fullName, abbreviation, lineNumber).ifPresent(results::add); + checkNonUtf8(fullName, abbreviation, lineNumber).ifPresent(results::add); + checkStartingLetters(fullName, abbreviation, lineNumber).ifPresent(results::add); + + // Warning checks + checkAbbreviationEqualsFullText(fullName, abbreviation, lineNumber).ifPresent(results::add); + checkOutdatedManagementAbbreviation(fullName, abbreviation, lineNumber).ifPresent(results::add); + + // Track for duplicate checks + fullNameToAbbrev.computeIfAbsent(fullName, k -> new ArrayList<>()).add(abbreviation); + abbrevToFullName.computeIfAbsent(abbreviation, k -> new ArrayList<>()).add(fullName); + + return results; + } +} diff --git a/src/test/java/org/jabref/logic/journals/JournalAbbreviationValidatorTest.java b/src/test/java/org/jabref/logic/journals/JournalAbbreviationValidatorTest.java new file mode 100644 index 00000000000..20a7b59f08d --- /dev/null +++ b/src/test/java/org/jabref/logic/journals/JournalAbbreviationValidatorTest.java @@ -0,0 +1,362 @@ +package org.jabref.logic.journals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JournalAbbreviationValidatorTest { + + private JournalAbbreviationValidator validator; + + @BeforeEach + void setUp() { + validator = new JournalAbbreviationValidator(); + } + + @Test + void checkWrongEscapeWithInvalidFullName() { + String fullName = "Zeszyty Naukowe Wy\\"; + String abbreviation = "Problemy Mat."; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Invalid escape sequence in full name at position 15", result.getMessage()); + assertEquals("Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX", result.getSuggestion()); + assertEquals(1, result.getLineNumber()); + } + + @Test + void checkWrongEscapeWithInvalidAbbreviation() { + String fullName = "Journal of Evolutionary Biochemistry and Physiology"; + String abbreviation = "J. Evol. Biochem. Physiol.\\"; + + var result = validator.checkWrongEscape(fullName, abbreviation, 2).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Invalid escape sequence in abbreviation at position 22", result.getMessage()); + assertEquals("Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX", result.getSuggestion()); + assertEquals(2, result.getLineNumber()); + } + + @Test + void checkWrongEscapeWithValidEscapes() { + String fullName = "Journal with \\n newline and \\t tab"; + String abbreviation = "J. with \\r return and \\b backspace"; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkWrongEscapeWithMultipleIssues() { + String fullName = "Journal with \\x invalid"; + String abbreviation = "J. with \\y invalid"; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Invalid escape sequence in full name at position 13", result.getMessage()); + assertEquals("Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX", result.getSuggestion()); + } + + @Test + void checkNonUtf8WithValidInput() { + String fullName = "Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkNonUtf8(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkNonUtf8WithInvalidInput() { + // Using a non-UTF8 character + String fullName = "Journal of Physics\uFFFD"; + String abbreviation = "J. Phys."; + + var result = validator.checkNonUtf8(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Journal name or abbreviation contains invalid UTF-8 sequences", result.getMessage()); + assertEquals("Ensure all characters are valid UTF-8. Remove or replace any invalid characters.", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithValidInput() { + String fullName = "Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithInvalidInput() { + String fullName = "Journal of Physics"; + String abbreviation = "Phys. J."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Abbreviation does not begin with same letter as full journal name", result.getMessage()); + assertEquals("Should start with 'j' (from 'Journal')", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithThePrefix() { + String fullName = "The Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithAPrefix() { + String fullName = "A Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkAbbreviationEqualsFullTextWithValidInput() { + String fullName = "Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkAbbreviationEqualsFullText(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkAbbreviationEqualsFullTextWithInvalidInput() { + String fullName = "Quantum"; + String abbreviation = "Quantum"; + + var result = validator.checkAbbreviationEqualsFullText(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Abbreviation is the same as the full text", result.getMessage()); + assertEquals("Consider using a shorter abbreviation to distinguish it from the full name", result.getSuggestion()); + } + + @Test + void checkOutdatedManagementAbbreviationWithValidInput() { + String fullName = "Management Science"; + String abbreviation = "Manag. Sci."; + + var result = validator.checkOutdatedManagementAbbreviation(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkOutdatedManagementAbbreviationWithInvalidInput() { + String fullName = "Management Science"; + String abbreviation = "Manage. Sci."; + + var result = validator.checkOutdatedManagementAbbreviation(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Management is abbreviated with outdated \"Manage.\" instead of \"Manag.\"", result.getMessage()); + assertEquals("Update to use the standard abbreviation \"Manag.\"", result.getSuggestion()); + } + + @Test + void checkDuplicateFullNames() { + // Add first entry + validator.validate("Journal of Physics", "J. Phys.", 1); + // Add duplicate with different abbreviation + validator.validate("Journal of Physics", "J. Phys. A", 2); + + var issues = validator.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Duplicate full name 'Journal of Physics' with different abbreviations: J. Phys., J. Phys. A", issues.get(0).getMessage()); + assertEquals("Consider consolidating abbreviations or using more specific full names", issues.get(0).getSuggestion()); + } + + @Test + void checkDuplicateAbbreviations() { + // Add first entry + validator.validate("Journal of Physics", "J. Phys.", 1); + // Add different journal with same abbreviation + validator.validate("Journal of Physiology", "J. Phys.", 2); + + var issues = validator.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Duplicate abbreviation 'J. Phys.' used for different journals: Journal of Physics; Journal of Physiology", issues.get(0).getMessage()); + assertEquals("Consider using more specific abbreviations to avoid ambiguity", issues.get(0).getSuggestion()); + } + + @Test + void checkWrongEscapeWithMultipleValidEscapes() { + String fullName = "Journal with \\n\\t\\r\\b\\f\\\"\\\\"; + String abbreviation = "J. with \\n\\t\\r\\b\\f\\\"\\\\"; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkWrongEscapeWithUnicodeEscape() { + String fullName = "Journal with \\u0041"; + String abbreviation = "J. with \\u0042"; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkWrongEscapeWithInvalidUnicodeEscape() { + String fullName = "Journal with \\u004"; + String abbreviation = "J. Phys."; + + var result = validator.checkWrongEscape(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Invalid escape sequence in full name at position 13", result.getMessage()); + assertEquals("Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX", result.getSuggestion()); + } + + @Test + void checkNonUtf8WithMultipleInvalidCharacters() { + String fullName = "Journal of Physics\uFFFD\uFFFD"; + String abbreviation = "J. Phys.\uFFFD"; + + var result = validator.checkNonUtf8(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Journal name or abbreviation contains invalid UTF-8 sequences", result.getMessage()); + assertEquals("Ensure all characters are valid UTF-8. Remove or replace any invalid characters.", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithMultiplePrefixes() { + String fullName = "The A An Journal of Physics"; + String abbreviation = "J. Phys."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithSpecialCharacters() { + String fullName = "The Journal of Physics & Chemistry"; + String abbreviation = "J. Phys. Chem."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void checkStartingLettersWithNumbers() { + String fullName = "2D Materials"; + String abbreviation = "2D Mater."; + + var result = validator.checkStartingLetters(fullName, abbreviation, 1).orElseThrow(); + assertEquals(true, result.isValid()); + assertEquals("", result.getMessage()); + assertEquals("", result.getSuggestion()); + } + + @Test + void validateWithEmptyInputs() { + var results = validator.validate("", "", 1); + assertEquals(5, results.size()); + assertEquals(false, results.get(0).isValid()); + } + + @Test + void validateWithWhitespaceOnly() { + var results = validator.validate(" ", " ", 1); + assertEquals(5, results.size()); + assertEquals(false, results.get(0).isValid()); + } + + @Test + void checkDuplicateFullNamesWithCaseInsensitivity() { + validator.validate("Journal of Physics", "J. Phys.", 1); + validator.validate("JOURNAL OF PHYSICS", "J. Phys.", 2); + + var issues = validator.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Duplicate full name 'JOURNAL OF PHYSICS' with different abbreviations: J. Phys., J. Phys.", issues.get(0).getMessage()); + } + + @Test + void checkDuplicateAbbreviationsWithCaseInsensitivity() { + validator.validate("Journal of Physics", "J. Phys.", 1); + validator.validate("Journal of Physiology", "j. phys.", 2); + + var issues = validator.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Duplicate abbreviation 'j. phys.' used for different journals: Journal of Physics; Journal of Physiology", issues.get(0).getMessage()); + } + + @Test + void checkAbbreviationEqualsFullTextWithSpecialCharacters() { + String fullName = "Physics & Chemistry"; + String abbreviation = "Physics & Chemistry"; + + var result = validator.checkAbbreviationEqualsFullText(fullName, abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Abbreviation is the same as the full text", result.getMessage()); + assertEquals("Consider using a shorter abbreviation to distinguish it from the full name", result.getSuggestion()); + } + + @Test + void checkOutdatedManagementAbbreviationWithVariations() { + String[] invalidAbbreviations = { + "Manage. Sci.", + "Manage Sci.", + "Manage.Sci.", + "Manage. Sci" + }; + + for (String abbreviation : invalidAbbreviations) { + var result = validator.checkOutdatedManagementAbbreviation("Management Science", abbreviation, 1).orElseThrow(); + assertEquals(false, result.isValid()); + assertEquals("Management is abbreviated with outdated \"Manage.\" instead of \"Manag.\"", result.getMessage()); + assertEquals("Update to use the standard abbreviation \"Manag.\"", result.getSuggestion()); + } + } + + @Test + void validateWithVeryLongInputs() { + String longName = "A".repeat(1000); + String longAbbr = "B".repeat(1000); + + var results = validator.validate(longName, longAbbr, 1); + assertEquals(5, results.size()); + assertEquals(false, results.get(0).isValid()); + } + + @Test + void validateWithAllChecks() { + String fullName = "Zeszyty Naukowe Wy\\"; + String abbreviation = "Problemy Mat."; + + var results = validator.validate(fullName, abbreviation, 1); + assertEquals(5, results.size()); + assertEquals(false, results.get(0).isValid()); + assertEquals("Invalid escape sequence in full name at position 15", results.get(0).getMessage()); + assertEquals("Use valid escape sequences: \\\\, \\\", \\n, \\t, \\r, \\b, \\f, \\uXXXX", results.get(0).getSuggestion()); + } +}