From d51b52bf52221330bf411f637ca7e5dd6868723f Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 17 Jun 2024 22:17:10 +0200 Subject: [PATCH] Fix cursor jumping (#11282) * Modernize tests in BibEntryWriterTest * Add tests for current behavior * Improve comments and code * Re-order tests * Extract method * Fix handling of ## * Fix detection of multiline fields * Fix space removal * Fix trimming of content * Codestyle: Tests * Remove standard cleanups * Add CHANGELOG.md entry * Right trim multiline fields * Add links to CHANGELOG.md * Test case cleanup * Fix whitespace formatting * Reformat comment (and add TODO marker) * More whitespace tweaking * Use our builder if possible * Fix whitespace at import * Try to get gradle executing all tests on GitHub CI * Tweak parallel tests * Optimize build * Trim whole field content * Fix variable name * Use `toList()` * More modern BibEntry generating * Better linebreaks * Fix logic * Fix an (unlreated) test * Update CHANGELOG.md * Update build.gradle * Use TrimWhitespaceFormatter() instead of internal "trim()" * Fix typo * Simplify code * Try to make code more readable * Add diffing for correct caret repositioning * Fix CHANGELOG * Improve code * Refine text * Fix binding issue * Fix BackupManager to modify the UI Co-authored-by: Christoph Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> * Works for simple case Co-authored-by: Christoph Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> * FieldFormatter not used at reading any more Co-authored-by: Christoph * Convert FieldContentFormatter to NormalizeWhitespaceFormatter Co-authored-by: Christoph * Make use of new change-aware bindings in other text-based editors, too Co-authored-by: Christoph * Fix tinylog * Fix when end is trimmed Co-authored-by: Christoph * Fix CHANGELOG.md Co-authored-by: Christoph * Adapt tests to new behavior Co-authored-by: Christoph * Fix imports * Fix paranthethes * Fix WTF * fix test * fix autosave thread * Remove dead code * Improve code - fix method names - Optional `get()` to `orElseThrow()` - List -> Collection - Remove obsoelte "COMMENT" and "CUSTOM_FIELD" property * Fix semantics of "VERBATIM" and remove 1:1 relation of property and field * Fix casing (and variable) * More FieldProperty cleanup * Enhanced KeyField support (required for customized entry types) * Fix fixing of double white spaces * Prepare: Fix cleanup of imported entries - introduce ParserFetcher interface - make ImportCleanup an abstract class - improve MathSicNet test code * Add TODOs * Add normalizing of white spaces to import - Consistent naming if "preferences" (not prefs, not preferenceService) * Really cleanup the entry * Adapt MathSciNetTest to new normalizing approach * Fix DBLP tests * Use "List.of()" instead of "Collections.singletonList" * Add @NonNull annotation * Remove obsolete JavaDoc comment * Fix NPE at tests * Incease heap space (again) * Fix missing dependency injection * Fix NPE * Fix checkstyle * Refine comment * Add more comments --------- Co-authored-by: Christoph Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> --- CHANGELOG.md | 5 +- build.gradle | 8 +- gradle.properties | 5 +- .../gui/autosaveandbackup/BackupManager.java | 18 +- .../jabref/gui/entryeditor/EntryEditor.java | 5 +- .../gui/entryeditor/RelatedArticlesTab.java | 12 +- .../gui/externalfiles/ImportHandler.java | 54 +++--- .../fieldeditors/AbstractEditorViewModel.java | 8 +- .../gui/fieldeditors/CitationKeyEditor.java | 2 +- .../jabref/gui/fieldeditors/DateEditor.java | 3 +- .../gui/fieldeditors/FieldEditorFX.java | 99 ++++++++++- .../jabref/gui/fieldeditors/FieldEditors.java | 11 +- .../jabref/gui/fieldeditors/ISSNEditor.java | 3 +- .../gui/fieldeditors/JournalEditor.java | 3 +- .../jabref/gui/fieldeditors/OwnerEditor.java | 3 +- .../gui/fieldeditors/PersonsEditor.java | 4 +- .../jabref/gui/fieldeditors/SimpleEditor.java | 11 +- .../jabref/gui/fieldeditors/UrlEditor.java | 3 +- .../identifier/IdentifierEditor.java | 2 +- .../gui/frame/JabRefFrameViewModel.java | 20 +-- .../gui/mergeentries/DiffHighlighting.java | 13 +- .../gui/mergeentries/FetchAndMergeEntry.java | 14 +- .../gui/shared/SharedDatabaseUIManager.java | 1 + .../jabref/logic/bibtex/BibEntryWriter.java | 21 +-- .../logic/bibtex/FieldContentFormatter.java | 56 ------ .../org/jabref/logic/bibtex/FieldWriter.java | 80 +++++---- .../logic/cleanup/FieldFormatterCleanup.java | 12 +- .../cleanup/NormalizeWhitespacesCleanup.java | 40 +++++ .../jabref/logic/database/DuplicateCheck.java | 24 +-- .../logic/exporter/BibDatabaseWriter.java | 33 ++-- .../logic/exporter/BibtexDatabaseWriter.java | 8 +- .../NormalizeWhitespaceFormatter.java | 62 +++++++ .../bibtexfields/TrimWhitespaceFormatter.java | 9 +- .../importer/EntryBasedParserFetcher.java | 21 +-- .../logic/importer/IdBasedParserFetcher.java | 21 +-- .../logic/importer/IdParserFetcher.java | 21 +-- .../jabref/logic/importer/ImportCleanup.java | 34 +++- .../logic/importer/ImportCleanupBiblatex.java | 8 +- .../logic/importer/ImportCleanupBibtex.java | 8 +- .../PagedSearchBasedParserFetcher.java | 2 +- .../jabref/logic/importer/ParserFetcher.java | 25 +++ .../importer/SearchBasedParserFetcher.java | 21 +-- .../logic/importer/fetcher/MathSciNet.java | 11 +- .../importer/fileformat/BibtexParser.java | 18 +- .../fileformat/MedlinePlainImporter.java | 2 +- .../logic/integrity/AmpersandChecker.java | 4 +- .../logic/integrity/BibStringChecker.java | 4 +- .../logic/integrity/HTMLCharacterChecker.java | 2 +- .../integrity/LatexIntegrityChecker.java | 4 +- .../jabref/logic/shared/DBMSSynchronizer.java | 8 +- .../model/database/KeyChangeListener.java | 20 +-- .../java/org/jabref/model/entry/BibEntry.java | 9 + .../java/org/jabref/model/entry/Month.java | 2 +- .../model/entry/field/FieldFactory.java | 31 ++-- .../model/entry/field/FieldProperty.java | 46 +++-- .../model/entry/field/InternalField.java | 4 +- .../model/entry/field/StandardField.java | 22 +-- .../model/entry/field/UnknownField.java | 2 +- .../entry/field/UserSpecificCommentField.java | 2 +- .../java/org/jabref/cli/JabRefCLITest.java | 4 +- .../CitationsRelationsTabViewModelTest.java | 7 + .../gui/externalfiles/ImportHandlerTest.java | 7 + .../logic/bibtex/BibEntryWriterTest.java | 166 +++++++++--------- .../jabref/logic/bibtex/FieldWriterTest.java | 76 ++++++-- .../logic/database/DuplicateCheckTest.java | 22 ++- .../exporter/BibtexDatabaseWriterTest.java | 77 +++++--- .../NormalizeWhitespaceFormatterTest.java} | 9 +- .../importer/fetcher/ArXivFetcherTest.java | 45 ++--- .../CompositeSearchBasedFetcherTest.java | 5 +- .../importer/fetcher/DBLPFetcherTest.java | 10 +- .../importer/fetcher/MathSciNetTest.java | 71 +++++--- .../SearchBasedFetcherCapabilityTest.java | 21 ++- .../fileformat/BibtexImporterTest.java | 29 +-- .../importer/fileformat/BibtexParserTest.java | 162 +++++++---------- .../fileformat/MedlinePlainImporterTest.java | 9 +- .../logic/shared/DBMSSynchronizerTest.java | 10 +- .../shared/SynchronizationSimulatorTest.java | 12 +- .../jabref/logic/util/io/FileUtilTest.java | 4 +- .../importer/fileformat/NbibImporterTest.bib | 5 +- 79 files changed, 1021 insertions(+), 734 deletions(-) delete mode 100644 src/main/java/org/jabref/logic/bibtex/FieldContentFormatter.java create mode 100644 src/main/java/org/jabref/logic/cleanup/NormalizeWhitespacesCleanup.java create mode 100644 src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java create mode 100644 src/main/java/org/jabref/logic/importer/ParserFetcher.java rename src/test/java/org/jabref/logic/{bibtex/FieldContentFormatterTest.java => formatter/bibtexfields/NormalizeWhitespaceFormatterTest.java} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3ec8f9ba1..5ba77a85c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,12 +40,15 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where drag and dropping entries from one library to another was not always working. [#11254](https://github.com/JabRef/jabref/issues/11254) - We fixed an issue where drag and dropping entries created a shallow copy. [#11160](https://github.com/JabRef/jabref/issues/11160) - We fixed an issue where imports to a custom group would only work for the first entry [#11085](https://github.com/JabRef/jabref/issues/11085), [#11269](https://github.com/JabRef/jabref/issues/11269) +- We fixed an issue when cursor jumped to the beginning of the line. [#5904](https://github.com/JabRef/jabref/issues/5904) - We fixed an issue where a new entry was not added to the selected group [#8933](https://github.com/JabRef/jabref/issues/8933) - We fixed an issue where the horizontal position of the Entry Preview inside the entry editor was not remembered across restarts [#11281](https://github.com/JabRef/jabref/issues/11281) - We fixed an issue where the search index was not updated after linking PDF files. [#11317](https://github.com/JabRef/jabref/pull/11317) - We fixed rendering of (first) author with a single letter surname. [forum#4330](https://discourse.jabref.org/t/correct-rendering-of-first-author-with-a-single-letter-surname/4330) +- We fixed that the import of the related articles tab sometimes used the wrong library mode. [#11282](https://github.com/JabRef/jabref/pull/11282) - We fixed an issue where the entry editor context menu was not shown correctly when JabRef is opened on a second, extended screen [#11323](https://github.com/JabRef/jabref/issues/11323), [#11174](https://github.com/JabRef/jabref/issues/11174) -- We fixe an issue where the value of "Override default font settings" was not applied on startup [#11344](https://github.com/JabRef/jabref/issues/11344) +- We fixed an issue where the value of "Override default font settings" was not applied on startup [#11344](https://github.com/JabRef/jabref/issues/11344) +- We fixed an issue when "Library changed on disk" appeared after a save by JabRef. [#4877](https://github.com/JabRef/jabref/issues/4877) ### Removed diff --git a/build.gradle b/build.gradle index 8a7257a3d33..96a2fedc716 100644 --- a/build.gradle +++ b/build.gradle @@ -530,9 +530,11 @@ testlogger { tasks.withType(Test) { reports.html.outputLocation.set(file("${reporting.baseDir}/${name}")) - // Enable parallel tests. See https://docs.gradle.org/8.1/userguide/performance.html#execute_tests_in_parallel for details. - maxParallelForks = Runtime.runtime.availableProcessors() - 1 - ignoreFailures = true + // Enable parallel tests (on desktop). + // See https://docs.gradle.org/8.1/userguide/performance.html#execute_tests_in_parallel for details. + if (!providers.environmentVariable("CI").isPresent()) { + maxParallelForks = Math.max(Runtime.runtime.availableProcessors() - 1, 1) + } } tasks.register('databaseTest', Test) { diff --git a/gradle.properties b/gradle.properties index 7de2be111f2..d0df6eb37db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,8 @@ org.gradle.vs.watch=true -# hint by https://docs.gradle.org/current/userguide/performance.html#increase_the_heap_size -org.gradle.jvmargs=-Xmx4096M +# Hint by https://docs.gradle.org/current/userguide/performance.html#increase_the_heap_size +# Otherwise, one gets "Java heap space" errors. +org.gradle.jvmargs=-Xmx6096M # hint by https://docs.gradle.org/current/userguide/performance.html#enable_configuration_cache # Does not work: diff --git a/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java b/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java index afce97681e6..b2538253ee7 100644 --- a/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java +++ b/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java @@ -32,8 +32,10 @@ import org.jabref.logic.util.BackupFileType; import org.jabref.logic.util.CoarseChangeFilter; import org.jabref.logic.util.io.BackupFileUtil; +import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.event.BibDatabaseContextChangedEvent; +import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.metadata.SaveOrder; import org.jabref.model.metadata.SelfContainedSaveOrder; @@ -67,7 +69,7 @@ public class BackupManager { private final LibraryTab libraryTab; // Contains a list of all backup paths - // During a write, the less recent backup file is deleted + // During writing, the less recent backup file is deleted private final Queue backupFilesQueue = new LinkedBlockingQueue<>(); private boolean needsBackup = false; @@ -259,10 +261,19 @@ void performBackup(Path backupPath) { .withSaveOrder(saveOrder) .withReformatOnSave(preferences.getLibraryPreferences().shouldAlwaysReformatOnSave()); + // "Clone" the database context + // We "know" that "only" the BibEntries might be changed during writing (see [org.jabref.logic.exporter.BibDatabaseWriter.savePartOfDatabase]) + List list = bibDatabaseContext.getDatabase().getEntries().stream() + .map(BibEntry::clone) + .map(BibEntry.class::cast) + .toList(); + BibDatabase bibDatabaseClone = new BibDatabase(list); + BibDatabaseContext bibDatabaseContextClone = new BibDatabaseContext(bibDatabaseClone, bibDatabaseContext.getMetaData()); + Charset encoding = bibDatabaseContext.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8); // We want to have successful backups only // Thus, we do not use a plain "FileWriter", but the "AtomicFileWriter" - // Example: What happens if one hard powers off the machine (or kills the jabref process) during the write of the backup? + // Example: What happens if one hard powers off the machine (or kills the jabref process) during writing of the backup? // This MUST NOT create a broken backup file that then jabref wants to "restore" from? try (Writer writer = new AtomicFileWriter(backupPath, encoding, false)) { BibWriter bibWriter = new BibWriter(writer, bibDatabaseContext.getDatabase().getNewLineSeparator()); @@ -272,7 +283,8 @@ void performBackup(Path backupPath) { preferences.getFieldPreferences(), preferences.getCitationKeyPatternPreferences(), entryTypesManager) - .saveDatabase(bibDatabaseContext); + // we save the clone to prevent the original database (and thus the UI) from being changed + .saveDatabase(bibDatabaseContextClone); backupFilesQueue.add(backupPath); // We wrote the file successfully diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index a879f8f4454..350c00b3f67 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -282,7 +282,7 @@ private List createTabs() { tabs.add(new SciteTab(preferencesService, taskExecutor, dialogService)); tabs.add(new CitationRelationsTab(dialogService, databaseContext, undoManager, stateManager, fileMonitor, preferencesService, libraryTab, taskExecutor)); - tabs.add(new RelatedArticlesTab(entryEditorPreferences, preferencesService, dialogService, taskExecutor)); + tabs.add(new RelatedArticlesTab(databaseContext, entryEditorPreferences, preferencesService, dialogService, taskExecutor)); sourceTab = new SourceTab( databaseContext, undoManager, @@ -361,9 +361,6 @@ private void adaptVisibleTabs() { } } - /** - * @return the currently edited entry - */ public BibEntry getCurrentlyEditedEntry() { return currentlyEditedEntry; } diff --git a/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java b/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java index abe7695e546..83428b3fb79 100644 --- a/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java @@ -26,8 +26,7 @@ import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.fetcher.MrDLibFetcher; import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabase; -import org.jabref.model.database.BibDatabaseModeDetection; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.preferences.MrDlibPreferences; @@ -47,13 +46,18 @@ public class RelatedArticlesTab extends EntryEditorTab { private final DialogService dialogService; private final TaskExecutor taskExecutor; + private final BibDatabaseContext databaseContext; + private final PreferencesService preferencesService; private final EntryEditorPreferences entryEditorPreferences; - public RelatedArticlesTab(EntryEditorPreferences entryEditorPreferences, + public RelatedArticlesTab(BibDatabaseContext databaseContext, + EntryEditorPreferences entryEditorPreferences, PreferencesService preferencesService, DialogService dialogService, TaskExecutor taskExecutor) { + this.databaseContext = databaseContext; + this.dialogService = dialogService; this.taskExecutor = taskExecutor; @@ -82,7 +86,7 @@ private StackPane getRelatedArticlesPane(BibEntry entry) { .wrap(() -> fetcher.performSearch(entry)) .onRunning(() -> progress.setVisible(true)) .onSuccess(relatedArticles -> { - ImportCleanup cleanup = ImportCleanup.targeting(BibDatabaseModeDetection.inferMode(new BibDatabase(List.of(entry)))); + ImportCleanup cleanup = ImportCleanup.targeting(databaseContext.getMode(), preferencesService.getFieldPreferences()); cleanup.doPostCleanup(relatedArticles); progress.setVisible(false); root.getChildren().add(getRelatedArticleInfo(relatedArticles, fetcher)); diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 96ad7e46e4d..91f20ed5ddc 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -65,7 +65,7 @@ public class ImportHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ImportHandler.class); private final BibDatabaseContext bibDatabaseContext; - private final PreferencesService preferencesService; + private final PreferencesService preferences; private final FileUpdateMonitor fileUpdateMonitor; private final ExternalFilesEntryLinker linker; private final ExternalFilesContentImporter contentImporter; @@ -75,7 +75,7 @@ public class ImportHandler { private final TaskExecutor taskExecutor; public ImportHandler(BibDatabaseContext database, - PreferencesService preferencesService, + PreferencesService preferences, FileUpdateMonitor fileupdateMonitor, UndoManager undoManager, StateManager stateManager, @@ -83,14 +83,14 @@ public ImportHandler(BibDatabaseContext database, TaskExecutor taskExecutor) { this.bibDatabaseContext = database; - this.preferencesService = preferencesService; + this.preferences = preferences; this.fileUpdateMonitor = fileupdateMonitor; this.stateManager = stateManager; this.dialogService = dialogService; this.taskExecutor = taskExecutor; - this.linker = new ExternalFilesEntryLinker(preferencesService.getFilePreferences(), database, dialogService); - this.contentImporter = new ExternalFilesContentImporter(preferencesService.getImportFormatPreferences()); + this.linker = new ExternalFilesEntryLinker(preferences.getFilePreferences(), database, dialogService); + this.contentImporter = new ExternalFilesContentImporter(preferences.getImportFormatPreferences()); this.undoManager = undoManager; } @@ -185,7 +185,7 @@ private BibEntry createEmptyEntryWithLink(Path file) { * There is no automatic download done. */ public void importEntries(List entries) { - ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode()); + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); cleanup.doPostCleanup(entries); importCleanedEntries(entries); } @@ -217,7 +217,7 @@ private void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext @VisibleForTesting BibEntry cleanUpEntry(BibDatabaseContext bibDatabaseContext, BibEntry entry) { - ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode()); + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); return cleanup.doPostCleanup(entry); } @@ -246,12 +246,12 @@ public Optional handleDuplicates(BibDatabaseContext bibDatabaseContext } public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, BibDatabaseContext bibDatabaseContext, DuplicateResolverDialog.DuplicateResolverResult decision) { - DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, bibDatabaseContext, stateManager, dialogService, preferencesService); + DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, bibDatabaseContext, stateManager, dialogService, preferences); if (decision == BREAK) { decision = dialogService.showCustomDialogAndWait(dialog).orElse(BREAK); } - if (preferencesService.getMergeDialogPreferences().shouldMergeApplyToAllEntries()) { - preferencesService.getMergeDialogPreferences().setAllEntriesDuplicateResolverDecision(decision); + if (preferences.getMergeDialogPreferences().shouldMergeApplyToAllEntries()) { + preferences.getMergeDialogPreferences().setAllEntriesDuplicateResolverDecision(decision); } return new DuplicateDecisionResult(decision, dialog.getMergedEntry()); } @@ -259,13 +259,13 @@ public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibE public void setAutomaticFields(List entries) { UpdateField.setAutomaticFields( entries, - preferencesService.getOwnerPreferences(), - preferencesService.getTimestampPreferences() + preferences.getOwnerPreferences(), + preferences.getTimestampPreferences() ); } public void downloadLinkedFiles(BibEntry entry) { - if (preferencesService.getFilePreferences().shouldDownloadLinkedFiles()) { + if (preferences.getFilePreferences().shouldDownloadLinkedFiles()) { entry.getFiles().stream() .filter(LinkedFile::isOnlineLink) .forEach(linkedFile -> @@ -275,7 +275,7 @@ public void downloadLinkedFiles(BibEntry entry) { bibDatabaseContext, taskExecutor, dialogService, - preferencesService + preferences ).download(false) ); } @@ -300,19 +300,19 @@ private void addToGroups(List entries, Collection group * @param entries entries to generate keys for */ private void generateKeys(List entries) { - if (!preferencesService.getImporterPreferences().isGenerateNewKeyOnImport()) { + if (!preferences.getImporterPreferences().isGenerateNewKeyOnImport()) { return; } CitationKeyGenerator keyGenerator = new CitationKeyGenerator( - bibDatabaseContext.getMetaData().getCiteKeyPatterns(preferencesService.getCitationKeyPatternPreferences() - .getKeyPatterns()), + bibDatabaseContext.getMetaData().getCiteKeyPatterns(preferences.getCitationKeyPatternPreferences() + .getKeyPatterns()), bibDatabaseContext.getDatabase(), - preferencesService.getCitationKeyPatternPreferences()); + preferences.getCitationKeyPatternPreferences()); entries.forEach(keyGenerator::generateAndSetKey); } public List handleBibTeXData(String entries) { - BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), fileUpdateMonitor); + BibtexParser parser = new BibtexParser(preferences.getImportFormatPreferences(), fileUpdateMonitor); try { List result = parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); Collection stringConstants = parser.getStringValues(); @@ -370,9 +370,9 @@ public List handleStringData(String data) throws FetcherException { private List tryImportFormats(String data) { try { ImportFormatReader importFormatReader = new ImportFormatReader( - preferencesService.getImporterPreferences(), - preferencesService.getImportFormatPreferences(), - preferencesService.getCitationKeyPatternPreferences(), + preferences.getImporterPreferences(), + preferences.getImportFormatPreferences(), + preferences.getCitationKeyPatternPreferences(), fileUpdateMonitor ); UnknownFormatImport unknownFormatImport = importFormatReader.importUnknownFormat(data); @@ -385,19 +385,19 @@ private List tryImportFormats(String data) { private List fetchByDOI(DOI doi) throws FetcherException { LOGGER.info("Found DOI identifier in clipboard"); - Optional entry = new DoiFetcher(preferencesService.getImportFormatPreferences()).performSearchById(doi.getDOI()); + Optional entry = new DoiFetcher(preferences.getImportFormatPreferences()).performSearchById(doi.getDOI()); return OptionalUtil.toList(entry); } private List fetchByArXiv(ArXivIdentifier arXivIdentifier) throws FetcherException { LOGGER.info("Found arxiv identifier in clipboard"); - Optional entry = new ArXivFetcher(preferencesService.getImportFormatPreferences()).performSearchById(arXivIdentifier.getNormalizedWithoutVersion()); + Optional entry = new ArXivFetcher(preferences.getImportFormatPreferences()).performSearchById(arXivIdentifier.getNormalizedWithoutVersion()); return OptionalUtil.toList(entry); } private List fetchByISBN(ISBN isbn) throws FetcherException { LOGGER.info("Found ISBN identifier in clipboard"); - Optional entry = new IsbnFetcher(preferencesService.getImportFormatPreferences()).performSearchById(isbn.getNormalized()); + Optional entry = new IsbnFetcher(preferences.getImportFormatPreferences()).performSearchById(isbn.getNormalized()); return OptionalUtil.toList(entry); } @@ -410,8 +410,8 @@ public void importEntriesWithDuplicateCheck(BibDatabaseContext database, List { if (newValue != null) { // A file may be loaded using CRLF. ControlsFX uses hardcoded \n for multiline fields. - // Normalizing is done during writing of the .bib file (see org.jabref.logic.exporter.BibWriter.BibWriter). // Thus, we need to normalize the line endings. - String oldValue = entry.getField(field).map(value -> value.replace("\r\n", "\n").trim()).orElse(null); - // Autosave and save action trigger the entry editor to reload the fields, so we have to - // check for changes here, otherwise the cursor position is annoyingly reset every few seconds - if (!(newValue.trim()).equals(oldValue)) { + // Note: Normalizing for the .bib file is done during writing of the .bib file (see org.jabref.logic.exporter.BibWriter.BibWriter). + String oldValue = entry.getField(field).map(value -> value.replace("\r\n", "\n")).orElse(null); + if (!newValue.equals(oldValue)) { entry.setField(field, newValue); undoManager.addEdit(new UndoableFieldChange(entry, field, oldValue, newValue)); } diff --git a/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java b/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java index 6466296e017..f351e0b083b 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java @@ -50,7 +50,7 @@ public CitationKeyEditor(Field field, undoManager, dialogService); - textField.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textField, viewModel.textProperty()); textField.initContextMenu(Collections::emptyList); diff --git a/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java b/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java index eb3a39e585e..58747ca2a3d 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java @@ -34,7 +34,8 @@ public DateEditor(Field field, DateTimeFormatter dateFormatter, SuggestionProvid this.viewModel = new DateEditorViewModel(field, suggestionProvider, dateFormatter, fieldCheckers, undoManager); datePicker.setStringConverter(viewModel.getDateToStringConverter()); - datePicker.getEditor().textProperty().bindBidirectional(viewModel.textProperty()); + + establishBinding(datePicker.getEditor(), viewModel.textProperty()); new EditorValidator(preferencesService).configureValidation(viewModel.getFieldValidator().getValidationStatus(), datePicker.getEditor()); } diff --git a/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java b/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java index f8fafb67800..3a4ec745712 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java +++ b/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java @@ -1,14 +1,107 @@ package org.jabref.gui.fieldeditors; +import java.util.Arrays; +import java.util.List; + +import javafx.application.Platform; +import javafx.beans.property.StringProperty; import javafx.scene.Parent; +import javafx.scene.control.TextInputControl; -import org.jabref.gui.util.ControlHelper; +import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.model.entry.BibEntry; +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.tobiasdiez.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public interface FieldEditorFX { void bindToEntry(BibEntry entry); + default void establishBinding(TextInputControl textInputControl, StringProperty viewModelTextProperty) { + // We need some more sophisticated handling to avoid cursor jumping + // https://github.com/JabRef/jabref/issues/5904 + + Logger logger = LoggerFactory.getLogger(FieldEditorFX.class); + + EasyBind.subscribe(viewModelTextProperty, newText -> { + // This might be triggered by save actions from a background thread, so we need to check if we are in the FX thread + if (Platform.isFxApplicationThread()) { + setTextAndUpdateCaretPosition(textInputControl, newText, logger); + } else { + DefaultTaskExecutor.runInJavaFXThread(() -> setTextAndUpdateCaretPosition(textInputControl, newText, logger)); + } + }); + EasyBind.subscribe(textInputControl.textProperty(), viewModelTextProperty::set); + } + + private void setTextAndUpdateCaretPosition(TextInputControl textInputControl, String newText, Logger logger) { + int lastCaretPosition = textInputControl.getCaretPosition(); + logger.trace("Caret at position {}", lastCaretPosition); + String oldText = textInputControl.getText(); + textInputControl.setText(newText); + logger.trace("listener triggered: '{}' -> '{}'", oldText, newText); + if (oldText == null) { + logger.trace("Empty field"); + return; + } + if (newText == null) { + logger.trace("Field cleared"); + return; + } + if (oldText.equals(newText)) { + logger.trace("No change, returned."); + return; + } + logger.trace("Trying to adapt..."); + // This is a special case when the text is set to a new value + // In this case, we want to adjust the caret position + List oldValueCharacters = Arrays.asList(oldText.split("")); + List newValueCharacters = Arrays.asList(newText.split("")); + List> deltaList = DiffUtils.diff(oldValueCharacters, newValueCharacters).getDeltas(); + logger.trace("Deltas: {}", deltaList); + AbstractDelta lastDelta = null; + for (AbstractDelta delta : deltaList) { + if (delta.getSource().getPosition() > lastCaretPosition) { + break; + } + lastDelta = delta; + } + if (lastDelta == null) { + // Change happened after current caret position + // Thus, simply restore the old position + textInputControl.positionCaret(lastCaretPosition); + } else { + logger.trace("Last Delta: {}", lastDelta); + logger.trace("Last Delta source: {}", lastDelta.getSource()); + logger.trace("Last Delta target: {}", lastDelta.getTarget()); + int offset = lastDelta.getTarget().getPosition() - lastDelta.getSource().getPosition(); + logger.trace("Offset before patching: {}", offset); + + switch (lastDelta.getType()) { + case DELETE: + offset -= lastDelta.getSource().size(); + break; + case INSERT: + offset += lastDelta.getTarget().size(); + break; + case CHANGE: + offset += lastDelta.getTarget().size() - lastDelta.getSource().size(); + break; + default: + break; + } + logger.trace("Offset after patching: {}", offset); + + int newCaretPosition = lastCaretPosition + offset; + textInputControl.positionCaret(newCaretPosition); + logger.trace("newCaretPosition: {}", newCaretPosition); + } + } + Parent getNode(); default void focus() { @@ -19,10 +112,6 @@ default void focus() { .requestFocus(); } - default boolean childIsFocused() { - return ControlHelper.childIsFocused(getNode()); - } - /** * Returns relative size of the field editor in terms of display space. *

diff --git a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java index c13974d2d85..c5501f1d4f2 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java +++ b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java @@ -71,27 +71,28 @@ public static FieldEditorFX getForField(final Field field, return new UrlEditor(field, suggestionProvider, fieldCheckers); } else if (fieldProperties.contains(FieldProperty.JOURNAL_NAME)) { return new JournalEditor(field, suggestionProvider, fieldCheckers); - } else if (fieldProperties.contains(FieldProperty.DOI) || fieldProperties.contains(FieldProperty.EPRINT) || fieldProperties.contains(FieldProperty.ISBN)) { + } else if (fieldProperties.contains(FieldProperty.IDENTIFIER) && field != StandardField.PMID || field == StandardField.ISBN) { + // Identifier editor does not support PMID, therefore excluded at the condition above return new IdentifierEditor(field, suggestionProvider, fieldCheckers); - } else if (fieldProperties.contains(FieldProperty.ISSN)) { + } else if (field == StandardField.ISSN) { return new ISSNEditor(field, suggestionProvider, fieldCheckers); } else if (field == StandardField.OWNER) { return new OwnerEditor(field, suggestionProvider, fieldCheckers); } else if (field == StandardField.GROUPS) { return new GroupEditor(field, suggestionProvider, fieldCheckers, preferences, isMultiLine, undoManager); - } else if (fieldProperties.contains(FieldProperty.FILE_EDITOR)) { + } else if (field == StandardField.FILE) { return new LinkedFilesEditor(field, databaseContext, suggestionProvider, fieldCheckers); } else if (fieldProperties.contains(FieldProperty.YES_NO)) { return new OptionEditor<>(new YesNoEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager)); } else if (fieldProperties.contains(FieldProperty.MONTH)) { return new OptionEditor<>(new MonthEditorViewModel(field, suggestionProvider, databaseContext.getMode(), fieldCheckers, undoManager)); - } else if (fieldProperties.contains(FieldProperty.GENDER)) { + } else if (field == StandardField.GENDER) { return new OptionEditor<>(new GenderEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager)); } else if (fieldProperties.contains(FieldProperty.EDITOR_TYPE)) { return new OptionEditor<>(new EditorTypeEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager)); } else if (fieldProperties.contains(FieldProperty.PAGINATION)) { return new OptionEditor<>(new PaginationEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager)); - } else if (fieldProperties.contains(FieldProperty.TYPE)) { + } else if (field == StandardField.TYPE) { if (entryType.equals(IEEETranEntryType.Patent)) { return new OptionEditor<>(new PatentTypeEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager)); } else { diff --git a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java index 7fc01a07c4a..351979d9ae9 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java @@ -53,7 +53,8 @@ public ISSNEditor(Field field, stateManager, preferencesService); - textArea.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textArea, viewModel.textProperty()); + textArea.initContextMenu(new DefaultMenu(textArea)); new EditorValidator(preferencesService).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea); diff --git a/src/main/java/org/jabref/gui/fieldeditors/JournalEditor.java b/src/main/java/org/jabref/gui/fieldeditors/JournalEditor.java index 4ab6f29f159..410c4e4b04d 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/JournalEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/JournalEditor.java @@ -50,7 +50,8 @@ public JournalEditor(Field field, dialogService, undoManager); - textField.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textField, viewModel.textProperty()); + textField.initContextMenu(new DefaultMenu(textField)); AutoCompletionTextInputBinding.autoComplete(textField, viewModel::complete); diff --git a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java index 48992341443..e7dfb69d2e6 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java @@ -33,7 +33,8 @@ public OwnerEditor(Field field, this.viewModel = new OwnerEditorViewModel(field, suggestionProvider, preferencesService, fieldCheckers, undoManager); - textArea.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textArea, viewModel.textProperty()); + textArea.initContextMenu(EditorMenus.getNameMenu(textArea)); new EditorValidator(preferencesService).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea); diff --git a/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java b/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java index 226f85bcf6a..f8d0009f8b8 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java @@ -32,7 +32,9 @@ public PersonsEditor(final Field field, textInput = isMultiLine ? new EditorTextArea() : new EditorTextField(); decoratedStringProperty = new UiThreadStringProperty(viewModel.textProperty()); - textInput.textProperty().bindBidirectional(decoratedStringProperty); + + establishBinding(textInput, decoratedStringProperty); + ((ContextMenuAddable) textInput).initContextMenu(EditorMenus.getNameMenu(textInput)); this.getChildren().add(textInput); diff --git a/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java b/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java index 49962de5525..dcd3940e34b 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java @@ -34,7 +34,8 @@ public SimpleEditor(final Field field, textInput = createTextInputControl(); HBox.setHgrow(textInput, Priority.ALWAYS); - textInput.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textInput, viewModel.textProperty()); + ((ContextMenuAddable) textInput).initContextMenu(new DefaultMenu(textInput)); this.getChildren().add(textInput); @@ -49,14 +50,6 @@ public SimpleEditor(final Field field, new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textInput); } - public SimpleEditor(final Field field, - final SuggestionProvider suggestionProvider, - final FieldCheckers fieldCheckers, - final PreferencesService preferences, - UndoManager undoManager) { - this(field, suggestionProvider, fieldCheckers, preferences, false, undoManager); - } - protected TextInputControl createTextInputControl() { return isMultiLine ? new EditorTextArea() : new EditorTextField(); } diff --git a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java index fd30c7a92b8..dd3ce38278d 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java @@ -42,7 +42,8 @@ public UrlEditor(Field field, this.viewModel = new UrlEditorViewModel(field, suggestionProvider, dialogService, preferencesService, fieldCheckers, undoManager); - textArea.textProperty().bindBidirectional(viewModel.textProperty()); + establishBinding(textArea, viewModel.textProperty()); + Supplier> contextMenuSupplier = EditorMenus.getCleanupUrlMenu(textArea); textArea.initContextMenu(contextMenuSupplier); diff --git a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java index 470390aeb3f..1f4891f2c5f 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java @@ -63,7 +63,7 @@ public IdentifierEditor(Field field, this.viewModel = new ISBNIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferencesService, undoManager, stateManager); case EPRINT -> this.viewModel = new EprintIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferencesService, undoManager); - + // TODO: Add support for PMID case null, default -> { assert field != null; throw new IllegalStateException("Unable to instantiate a view model for identifier field editor '%s'".formatted(field.getDisplayName())); diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java index 94d52b6617b..a9d907a03c3 100644 --- a/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java +++ b/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java @@ -44,7 +44,7 @@ public class JabRefFrameViewModel { private static final Logger LOGGER = LoggerFactory.getLogger(JabRefFrameViewModel.class); - private final PreferencesService prefs; + private final PreferencesService preferences; private final StateManager stateManager; private final DialogService dialogService; private final LibraryTabContainer tabContainer; @@ -61,7 +61,7 @@ public JabRefFrameViewModel(PreferencesService preferencesService, FileUpdateMonitor fileUpdateMonitor, UndoManager undoManager, TaskExecutor taskExecutor) { - this.prefs = preferencesService; + this.preferences = preferencesService; this.stateManager = stateManager; this.dialogService = dialogService; this.tabContainer = tabContainer; @@ -72,14 +72,14 @@ public JabRefFrameViewModel(PreferencesService preferencesService, } void storeLastOpenedFiles(List filenames, Path focusedDatabase) { - if (prefs.getWorkspacePreferences().shouldOpenLastEdited()) { + if (preferences.getWorkspacePreferences().shouldOpenLastEdited()) { // Here we store the names of all current files. If there is no current file, we remove any // previously stored filename. if (filenames.isEmpty()) { - prefs.getGuiPreferences().getLastFilesOpened().clear(); + preferences.getGuiPreferences().getLastFilesOpened().clear(); } else { - prefs.getGuiPreferences().setLastFilesOpened(filenames); - prefs.getGuiPreferences().setLastFocusedFile(focusedDatabase); + preferences.getGuiPreferences().setLastFilesOpened(filenames); + preferences.getGuiPreferences().setLastFocusedFile(focusedDatabase); } } } @@ -177,8 +177,8 @@ private void openDatabases(List parserResults) { Path focusedFile = parserResults.stream() .findFirst() .flatMap(ParserResult::getPath) - .orElse(prefs.getGuiPreferences() - .getLastFocusedFile()) + .orElse(preferences.getGuiPreferences() + .getLastFocusedFile()) .toAbsolutePath(); // Add all bibDatabases databases to the frame: @@ -195,7 +195,7 @@ private void openDatabases(List parserResults) { parserResult, tabContainer, dialogService, - prefs, + preferences, stateManager, entryTypesManager, fileUpdateMonitor, @@ -372,7 +372,7 @@ private void jumpToEntry(String entryKey) { */ void addImportedEntries(final LibraryTab tab, final ParserResult parserResult) { BackgroundTask task = BackgroundTask.wrap(() -> parserResult); - ImportCleanup cleanup = ImportCleanup.targeting(tab.getBibDatabaseContext().getMode()); + ImportCleanup cleanup = ImportCleanup.targeting(tab.getBibDatabaseContext().getMode(), preferences.getFieldPreferences()); cleanup.doPostCleanup(parserResult.getDatabase().getEntries()); ImportEntriesDialog dialog = new ImportEntriesDialog(tab.getBibDatabaseContext(), task); dialog.setTitle(Localization.lang("Import")); diff --git a/src/main/java/org/jabref/gui/mergeentries/DiffHighlighting.java b/src/main/java/org/jabref/gui/mergeentries/DiffHighlighting.java index 7f36c651683..06c6c0cd1f7 100644 --- a/src/main/java/org/jabref/gui/mergeentries/DiffHighlighting.java +++ b/src/main/java/org/jabref/gui/mergeentries/DiffHighlighting.java @@ -1,7 +1,6 @@ package org.jabref.gui.mergeentries; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -16,11 +15,11 @@ private DiffHighlighting() { } public static List generateDiffHighlighting(String baseString, String modifiedString, String separator) { - List stringList = Arrays.asList(baseString.split(separator)); - List result = stringList.stream().map(text -> forUnchanged(text + separator)).collect(Collectors.toList()); - List> deltaList = DiffUtils.diff(stringList, Arrays.asList(modifiedString.split(separator))).getDeltas(); - Collections.reverse(deltaList); - for (AbstractDelta delta : deltaList) { + List baseStringSplit = Arrays.asList(baseString.split(separator)); + List modifiedStringSplit = Arrays.asList(modifiedString.split(separator)); + List> deltaList = DiffUtils.diff(baseStringSplit, modifiedStringSplit).getDeltas(); + List result = baseStringSplit.stream().map(text -> forUnchanged(text + separator)).collect(Collectors.toList()); + for (AbstractDelta delta : deltaList.reversed()) { int startPos = delta.getSource().getPosition(); List lines = delta.getSource().getLines(); int offset = 0; @@ -30,7 +29,7 @@ public static List generateDiffHighlighting(String baseString, String modi result.set(startPos + offset, forRemoved(line + separator)); offset++; } - result.set(startPos + offset - 1, forRemoved(stringList.get((startPos + offset) - 1) + separator)); + result.set(startPos + offset - 1, forRemoved(baseStringSplit.get((startPos + offset) - 1) + separator)); result.add(startPos + offset, forAdded(String.join(separator, delta.getTarget().getLines()))); break; case DELETE: diff --git a/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java b/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java index c5c65b1cc5d..2864c26872d 100644 --- a/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java +++ b/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java @@ -47,16 +47,16 @@ public class FetchAndMergeEntry { private final UndoManager undoManager; private final BibDatabaseContext bibDatabaseContext; private final TaskExecutor taskExecutor; - private final PreferencesService preferencesService; + private final PreferencesService preferences; public FetchAndMergeEntry(BibDatabaseContext bibDatabaseContext, TaskExecutor taskExecutor, - PreferencesService preferencesService, + PreferencesService preferences, DialogService dialogService, UndoManager undoManager) { this.bibDatabaseContext = bibDatabaseContext; this.taskExecutor = taskExecutor; - this.preferencesService = preferencesService; + this.preferences = preferences; this.dialogService = dialogService; this.undoManager = undoManager; } @@ -73,11 +73,11 @@ public void fetchAndMerge(BibEntry entry, List fields) { for (Field field : fields) { Optional fieldContent = entry.getField(field); if (fieldContent.isPresent()) { - Optional fetcher = WebFetchers.getIdBasedFetcherForField(field, preferencesService.getImportFormatPreferences()); + Optional fetcher = WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences()); if (fetcher.isPresent()) { BackgroundTask.wrap(() -> fetcher.get().performSearchById(fieldContent.get())) .onSuccess(fetchedEntry -> { - ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode()); + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); String type = field.getDisplayName(); if (fetchedEntry.isPresent()) { cleanup.doPostCleanup(fetchedEntry.get()); @@ -105,7 +105,7 @@ public void fetchAndMerge(BibEntry entry, List fields) { } private void showMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { - MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferencesService); + MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferences); dialog.setTitle(Localization.lang("Merge entry with %0 information", fetcher.getName())); dialog.setLeftHeaderText(Localization.lang("Original entry")); dialog.setRightHeaderText(Localization.lang("Entry from %0", fetcher.getName())); @@ -169,7 +169,7 @@ public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) { BackgroundTask.wrap(() -> fetcher.performSearch(entry).stream().findFirst()) .onSuccess(fetchedEntry -> { if (fetchedEntry.isPresent()) { - ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode()); + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); cleanup.doPostCleanup(fetchedEntry.get()); showMergeDialog(entry, fetchedEntry.get(), fetcher); } else { diff --git a/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java b/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java index 83536364a88..a8f7f0a9163 100644 --- a/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java +++ b/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java @@ -209,6 +209,7 @@ private BibDatabaseContext getBibDatabaseContextForSharedDatabase() { DBMSSynchronizer synchronizer = new DBMSSynchronizer( bibDatabaseContext, preferencesService.getBibEntryPreferences().getKeywordSeparator(), + preferencesService.getFieldPreferences(), preferencesService.getCitationKeyPatternPreferences().getKeyPatterns(), fileUpdateMonitor); bibDatabaseContext.convertToSharedDatabase(synchronizer); diff --git a/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java b/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java index fc83d421578..2bdae3666ca 100644 --- a/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java +++ b/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java @@ -104,16 +104,16 @@ private void writeRequiredFieldsFirstRemainingFieldsSecond(BibEntry entry, BibWr if (type.isPresent()) { // Write required fields first List requiredFields = type.get() - .getRequiredFields() - .stream() - .map(OrFields::getFields) - .flatMap(Collection::stream) - .sorted(Comparator.comparing(Field::getName)) - .collect(Collectors.toList()); - + .getRequiredFields() + .stream() + .map(OrFields::getFields) + .flatMap(Collection::stream) + .sorted(Comparator.comparing(Field::getName)) + .toList(); for (Field field : requiredFields) { writeField(entry, out, field, indent); } + written.addAll(requiredFields); // Then optional fields List optionalFields = type.get() @@ -121,21 +121,18 @@ private void writeRequiredFieldsFirstRemainingFieldsSecond(BibEntry entry, BibWr .stream() .map(BibField::field) .sorted(Comparator.comparing(Field::getName)) - .collect(Collectors.toList()); - + .toList(); for (Field field : optionalFields) { writeField(entry, out, field, indent); } - - written.addAll(requiredFields); written.addAll(optionalFields); } + // Then write remaining fields in alphabetic order. SortedSet remainingFields = entry.getFields() .stream() .filter(key -> !written.contains(key)) .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Field::getName)))); - for (Field field : remainingFields) { writeField(entry, out, field, indent); } diff --git a/src/main/java/org/jabref/logic/bibtex/FieldContentFormatter.java b/src/main/java/org/jabref/logic/bibtex/FieldContentFormatter.java deleted file mode 100644 index 726ea97de55..00000000000 --- a/src/main/java/org/jabref/logic/bibtex/FieldContentFormatter.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.jabref.logic.bibtex; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; - -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.StandardField; - -/** - * This class provides the reformatting needed when reading BibTeX fields formatted - * in JabRef style. The reformatting must undo all formatting done by JabRef when - * writing the same fields. - */ -public class FieldContentFormatter { - - // 's' matches a space, tab, new line, carriage return. - private static final Pattern WHITESPACE = Pattern.compile("\\s+"); - - private final Set multiLineFields; - - public FieldContentFormatter(FieldPreferences preferences) { - Objects.requireNonNull(preferences); - - multiLineFields = new HashSet<>(); - // the following two are also coded in org.jabref.logic.bibtex.LatexFieldFormatter.format(String, String) - multiLineFields.add(StandardField.ABSTRACT); - multiLineFields.add(StandardField.COMMENT); - multiLineFields.add(StandardField.REVIEW); - // the file field should not be formatted, therefore we treat it as a multi line field - multiLineFields.addAll(preferences.getNonWrappableFields()); - } - - /** - * Performs the reformatting - * - * @param fieldContent the content to format - * @param field the name of the bibtex field - * @return the formatted field content. - */ - public String format(String fieldContent, Field field) { - if (multiLineFields.contains(field)) { - // Keep the field as is. - // Newlines are normalized at org.jabref.logic.exporter.BibWriter - // Alternative: StringUtil.unifyLineBreaks(fieldContent, OS.NEWLINE) - return fieldContent; - } - - return WHITESPACE.matcher(fieldContent).replaceAll(" "); - } - - public String format(StringBuilder fieldContent, Field field) { - return format(fieldContent.toString(), field); - } -} diff --git a/src/main/java/org/jabref/logic/bibtex/FieldWriter.java b/src/main/java/org/jabref/logic/bibtex/FieldWriter.java index 50c6a3180e2..1a6702242eb 100644 --- a/src/main/java/org/jabref/logic/bibtex/FieldWriter.java +++ b/src/main/java/org/jabref/logic/bibtex/FieldWriter.java @@ -21,7 +21,6 @@ public class FieldWriter { private final boolean neverFailOnHashes; private final FieldPreferences preferences; - private final FieldContentFormatter formatter; public FieldWriter(FieldPreferences preferences) { this(true, preferences); @@ -30,8 +29,6 @@ public FieldWriter(FieldPreferences preferences) { private FieldWriter(boolean neverFailOnHashes, FieldPreferences preferences) { this.neverFailOnHashes = neverFailOnHashes; this.preferences = preferences; - - formatter = new FieldContentFormatter(preferences); } public static FieldWriter buildIgnoreHashes(FieldPreferences prefs) { @@ -79,11 +76,12 @@ private static void checkBraces(String text) throws InvalidFieldValueException { * @param field the name of the field - used to trigger different serializations, e.g., turning off resolution for some strings * @param content the content of the field * @return a formatted string suitable for output - * @throws InvalidFieldValueException if s is not a correct bibtex string, e.g., because of improperly balanced braces or using # not paired + * + * @throws InvalidFieldValueException if content is not a correct bibtex string, e.g., because of improperly balanced braces or using # not paired */ public String write(Field field, String content) throws InvalidFieldValueException { if (content == null) { - return FIELD_START + String.valueOf(FIELD_END); + return FIELD_START + "" + FIELD_END; } if (!shouldResolveStrings(field) || field.equals(InternalField.BIBTEX_STRING)) { @@ -101,6 +99,8 @@ public String write(Field field, String content) throws InvalidFieldValueExcepti private String formatAndResolveStrings(String content, Field field) throws InvalidFieldValueException { checkBraces(content); + content = content.replace("##", ""); + StringBuilder stringBuilder = new StringBuilder(); // Here we assume that the user encloses any bibtex strings in #, e.g.: @@ -109,39 +109,30 @@ private String formatAndResolveStrings(String content, Field field) throws Inval // jan # { - } # feb int pivot = 0; while (pivot < content.length()) { - int goFrom = pivot; - int pos1 = pivot; - while (goFrom == pos1) { - pos1 = content.indexOf(BIBTEX_STRING_START_END_SYMBOL, goFrom); - if ((pos1 > 0) && (content.charAt(pos1 - 1) == '\\')) { - goFrom = pos1 + 1; - pos1++; - } else { - goFrom = pos1 - 1; // Ends the loop. - } - } - + int pos1 = getFirstOccurrenceOfStartEndSymbol(content, pivot); int pos2; if (pos1 == -1) { - pos1 = content.length(); // No more occurrences found. + // Process content and end the loop after that + pos1 = content.length(); pos2 = -1; } else { pos2 = content.indexOf(BIBTEX_STRING_START_END_SYMBOL, pos1 + 1); - if (pos2 == -1) { - if (neverFailOnHashes) { - pos1 = content.length(); // just write out the rest of the text, and throw no exception - } else { - LOGGER.error("The character {} is not allowed in BibTeX strings unless escaped as in '\\{}'. " - + "In JabRef, use pairs of # characters to indicate a string. " - + "Note that the entry causing the problem has been selected. Field value: {}", - BIBTEX_STRING_START_END_SYMBOL, - BIBTEX_STRING_START_END_SYMBOL, - content); - throw new InvalidFieldValueException( - "The character " + BIBTEX_STRING_START_END_SYMBOL + " is not allowed in BibTeX strings unless escaped as in '\\" + BIBTEX_STRING_START_END_SYMBOL + "'.\n" - + "In JabRef, use pairs of # characters to indicate a string.\n" - + "Note that the entry causing the problem has been selected. Field value: " + content); - } + } + + if (pos2 == -1) { + if (neverFailOnHashes) { + pos1 = content.length(); // just write out the rest of the text, and throw no exception + } else { + LOGGER.error("The character {} is not allowed in BibTeX strings unless escaped as in '\\{}'. " + + "In JabRef, use pairs of # characters to indicate a string. " + + "Note that the entry causing the problem has been selected. Field value: {}", + BIBTEX_STRING_START_END_SYMBOL, + BIBTEX_STRING_START_END_SYMBOL, + content); + throw new InvalidFieldValueException( + "The character " + BIBTEX_STRING_START_END_SYMBOL + " is not allowed in BibTeX strings unless escaped as in '\\" + BIBTEX_STRING_START_END_SYMBOL + "'.\n" + + "In JabRef, use pairs of # characters to indicate a string.\n" + + "Note that the entry causing the problem has been selected. Field value: " + content); } } @@ -163,7 +154,25 @@ private String formatAndResolveStrings(String content, Field field) throws Inval } } - return formatter.format(stringBuilder, field); + return stringBuilder.toString(); + } + + /** + * Finds the first occurrence of # from the pivot point + */ + private static int getFirstOccurrenceOfStartEndSymbol(String content, int pivot) { + int goFrom = pivot; + int pos1 = pivot; + while (goFrom == pos1) { + pos1 = content.indexOf(BIBTEX_STRING_START_END_SYMBOL, goFrom); + if ((pos1 > 0) && (content.charAt(pos1 - 1) == '\\')) { + pos1++; + goFrom = pos1; + } else { + break; + } + } + return pos1; } private boolean shouldResolveStrings(Field field) { @@ -176,9 +185,8 @@ private boolean shouldResolveStrings(Field field) { private String formatWithoutResolvingStrings(String content, Field field) throws InvalidFieldValueException { checkBraces(content); - StringBuilder stringBuilder = new StringBuilder(String.valueOf(FIELD_START)); - stringBuilder.append(formatter.format(content, field)); + stringBuilder.append(content); stringBuilder.append(FIELD_END); return stringBuilder.toString(); } diff --git a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanup.java b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanup.java index 197debd20fd..3528cdc5df9 100644 --- a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanup.java +++ b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanup.java @@ -1,7 +1,6 @@ package org.jabref.logic.cleanup; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -50,15 +49,14 @@ public List cleanup(BibEntry entry) { private List cleanupSingleField(Field fieldKey, BibEntry entry) { if (!entry.hasField(fieldKey)) { // Not set -> nothing to do - return Collections.emptyList(); + return List.of(); } - String oldValue = entry.getField(fieldKey).orElse(null); - // Run formatter + String oldValue = entry.getField(fieldKey).orElse(null); String newValue = formatter.format(oldValue); if (newValue.equals(oldValue)) { - return Collections.emptyList(); + return List.of(); } else { if (newValue.isEmpty()) { entry.clearField(fieldKey); @@ -67,7 +65,7 @@ private List cleanupSingleField(Field fieldKey, BibEntry entry) { entry.setField(fieldKey, newValue, EntriesEventSource.SAVE_ACTION); } FieldChange change = new FieldChange(entry, fieldKey, oldValue, newValue); - return Collections.singletonList(change); + return List.of(change); } } @@ -86,7 +84,7 @@ private List cleanupAllFields(BibEntry entry) { private List cleanupAllTextFields(BibEntry entry) { List fieldChanges = new ArrayList<>(); Set fields = new HashSet<>(entry.getFields()); - FieldFactory.getNotTextFieldNames().forEach(fields::remove); + FieldFactory.getNotTextFields().forEach(fields::remove); for (Field fieldKey : fields) { if (!fieldKey.equals(InternalField.KEY_FIELD)) { fieldChanges.addAll(cleanupSingleField(fieldKey, entry)); diff --git a/src/main/java/org/jabref/logic/cleanup/NormalizeWhitespacesCleanup.java b/src/main/java/org/jabref/logic/cleanup/NormalizeWhitespacesCleanup.java new file mode 100644 index 00000000000..bebdce4534a --- /dev/null +++ b/src/main/java/org/jabref/logic/cleanup/NormalizeWhitespacesCleanup.java @@ -0,0 +1,40 @@ +package org.jabref.logic.cleanup; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.formatter.bibtexfields.NormalizeWhitespaceFormatter; +import org.jabref.model.FieldChange; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +public class NormalizeWhitespacesCleanup implements CleanupJob { + + private static final Collection NO_TEXT_FIELDS = FieldFactory.getNotTextFields(); + + private final NormalizeWhitespaceFormatter formatter; + + public NormalizeWhitespacesCleanup(FieldPreferences fieldPreferences) { + formatter = new NormalizeWhitespaceFormatter(fieldPreferences); + } + + @Override + public List cleanup(BibEntry entry) { + List changes = new ArrayList<>(); + for (Field field : entry.getFields()) { + if (NO_TEXT_FIELDS.contains(field)) { + continue; + } + // We are sure that the field is set, because this is the assertion of getFields() + String oldValue = entry.getField(field).orElseThrow(); + String newValue = formatter.format(oldValue, field); + if (!newValue.equals(oldValue)) { + entry.setField(field, newValue).ifPresent(changes::add); + } + } + return changes; + } +} diff --git a/src/main/java/org/jabref/logic/database/DuplicateCheck.java b/src/main/java/org/jabref/logic/database/DuplicateCheck.java index 2f3bb77b0be..e3fbdc3ed99 100644 --- a/src/main/java/org/jabref/logic/database/DuplicateCheck.java +++ b/src/main/java/org/jabref/logic/database/DuplicateCheck.java @@ -3,7 +3,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -21,11 +20,9 @@ import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.entry.field.BibField; 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.OrFields; import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.identifier.DOI; import org.jabref.model.entry.identifier.ISBN; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.strings.StringUtil; @@ -76,12 +73,9 @@ public DuplicateCheck(BibEntryTypesManager entryTypesManager) { } private static boolean haveSameIdentifier(final BibEntry one, final BibEntry two) { - for (final Field name : FieldFactory.getIdentifierFieldNames()) { - if (one.getField(name).isPresent() && one.getField(name).equals(two.getField(name))) { - return true; - } - } - return false; + return one.getFields().stream() + .filter(field -> field.getProperties().contains(FieldProperty.IDENTIFIER)) + .anyMatch(field -> two.getField(field).map(content -> one.getField(field).orElseThrow().equals(content)).orElse(false)); } private static boolean haveDifferentEntryType(final BibEntry one, final BibEntry two) { @@ -323,17 +317,11 @@ private static double similarity(final String first, final String second) { * Checks if the two entries represent the same publication. */ public boolean isDuplicate(final BibEntry one, final BibEntry two, final BibDatabaseMode bibDatabaseMode) { + // Checks DOI and other identifiers if (haveSameIdentifier(one, two)) { return true; } - // check DOI - Optional oneDOI = one.getDOI(); - Optional twoDOI = two.getDOI(); - if (oneDOI.isPresent() && twoDOI.isPresent()) { - return Objects.equals(oneDOI, twoDOI); - } - if (haveDifferentEntryType(one, two) || haveDifferentEditions(one, two) || haveDifferentChaptersOrPagesOfTheSameBook(one, two)) { @@ -341,12 +329,12 @@ public boolean isDuplicate(final BibEntry one, final BibEntry two, final BibData } // In case an ISBN is present, it is a strong indicator that the entries are equal. - // Only in InBook, InCollection, or Article the ISBN may be equal and the puplication on different pages (and thus not equal) + // Only in InBook, InCollection, or Article the ISBN may be equal and the publication on different pages (and thus not equal) Optional oneISBN = one.getISBN(); Optional twoISBN = two.getISBN(); if (oneISBN.isPresent() && twoISBN.isPresent() && Objects.equals(oneISBN, twoISBN) - && !List.of(StandardEntryType.Article, StandardEntryType.InBook, StandardEntryType.InCollection).contains(one.getType())) { + && !Set.of(StandardEntryType.Article, StandardEntryType.InBook, StandardEntryType.InCollection).contains(one.getType())) { return true; } diff --git a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java index 15a63cdd0fa..c8fc121184e 100644 --- a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java +++ b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java @@ -15,8 +15,8 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Stream; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.bibtex.comparator.BibtexStringComparator; import org.jabref.logic.bibtex.comparator.CrossRefEntryComparator; import org.jabref.logic.bibtex.comparator.FieldComparator; @@ -27,6 +27,7 @@ import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; import org.jabref.logic.cleanup.FieldFormatterCleanup; import org.jabref.logic.cleanup.FieldFormatterCleanups; +import org.jabref.logic.cleanup.NormalizeWhitespacesCleanup; import org.jabref.logic.formatter.bibtexfields.TrimWhitespaceFormatter; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabase; @@ -62,19 +63,22 @@ public enum SaveType { WITH_JABREF_META_DATA, PLAIN_BIBTEX } protected final CitationKeyPatternPreferences keyPatternPreferences; protected final List saveActionsFieldChanges = new ArrayList<>(); protected final BibEntryTypesManager entryTypesManager; + protected final FieldPreferences fieldPreferences; public BibDatabaseWriter(BibWriter bibWriter, SelfContainedSaveConfiguration saveConfiguration, + FieldPreferences fieldPreferences, CitationKeyPatternPreferences keyPatternPreferences, BibEntryTypesManager entryTypesManager) { this.bibWriter = Objects.requireNonNull(bibWriter); this.saveConfiguration = saveConfiguration; this.keyPatternPreferences = keyPatternPreferences; + this.fieldPreferences = fieldPreferences; this.entryTypesManager = entryTypesManager; assert saveConfiguration.getSaveOrder().getOrderType() != SaveOrder.OrderType.TABLE; } - private static List applySaveActions(List toChange, MetaData metaData) { + private static List applySaveActions(List toChange, MetaData metaData, FieldPreferences fieldPreferences) { List changes = new ArrayList<>(); Optional saveActions = metaData.getSaveActions(); @@ -85,22 +89,22 @@ private static List applySaveActions(List toChange, MetaD } }); - // Run standard cleanups - List preSaveCleanups = - Stream.of(new TrimWhitespaceFormatter()) - .map(formatter -> new FieldFormatterCleanup(InternalField.INTERNAL_ALL_FIELD, formatter)) - .toList(); - for (FieldFormatterCleanup formatter : preSaveCleanups) { - for (BibEntry entry : toChange) { - changes.addAll(formatter.cleanup(entry)); + // Trim and normalize all white spaces + FieldFormatterCleanup trimWhiteSpaces = new FieldFormatterCleanup(InternalField.INTERNAL_ALL_FIELD, new TrimWhitespaceFormatter()); + NormalizeWhitespacesCleanup normalizeWhitespacesCleanup = new NormalizeWhitespacesCleanup(fieldPreferences); + for (BibEntry entry : toChange) { + // Only apply the trimming if the entry itself has other changes (e.g., by the user or by save actions) + if (entry.hasChanged()) { + changes.addAll(trimWhiteSpaces.cleanup(entry)); + changes.addAll(normalizeWhitespacesCleanup.cleanup(entry)); } } return changes; } - public static List applySaveActions(BibEntry entry, MetaData metaData) { - return applySaveActions(Collections.singletonList(entry), metaData); + public static List applySaveActions(BibEntry entry, MetaData metaData, FieldPreferences fieldPreferences) { + return applySaveActions(List.of(entry), metaData, fieldPreferences); } private static List> getSaveComparators(SaveOrder saveOrder) { @@ -182,7 +186,10 @@ public void savePartOfDatabase(BibDatabaseContext bibDatabaseContext, List sortedEntries = getSortedEntries(entries, saveConfiguration.getSelfContainedSaveOrder()); - List saveActionChanges = applySaveActions(sortedEntries, bibDatabaseContext.getMetaData()); + + // FIXME: "Clean" architecture violation: We modify the entries here, which should not happen during a write + // The cleanup should be done before the write operation + List saveActionChanges = applySaveActions(sortedEntries, bibDatabaseContext.getMetaData(), fieldPreferences); saveActionsFieldChanges.addAll(saveActionChanges); if (keyPatternPreferences.shouldGenerateCiteKeysBeforeSaving()) { List keyChanges = generateCitationKeys(bibDatabaseContext, sortedEntries); diff --git a/src/main/java/org/jabref/logic/exporter/BibtexDatabaseWriter.java b/src/main/java/org/jabref/logic/exporter/BibtexDatabaseWriter.java index 08bdede5b93..2c1a1533083 100644 --- a/src/main/java/org/jabref/logic/exporter/BibtexDatabaseWriter.java +++ b/src/main/java/org/jabref/logic/exporter/BibtexDatabaseWriter.java @@ -38,8 +38,6 @@ public class BibtexDatabaseWriter extends BibDatabaseWriter { private static final String PREAMBLE_PREFIX = "@Preamble"; private static final String STRING_PREFIX = "@String"; - private final FieldPreferences fieldPreferences; - public BibtexDatabaseWriter(BibWriter bibWriter, SelfContainedSaveConfiguration saveConfiguration, FieldPreferences fieldPreferences, @@ -47,10 +45,9 @@ public BibtexDatabaseWriter(BibWriter bibWriter, BibEntryTypesManager entryTypesManager) { super(bibWriter, saveConfiguration, + fieldPreferences, citationKeyPatternPreferences, entryTypesManager); - - this.fieldPreferences = fieldPreferences; } public BibtexDatabaseWriter(Writer writer, @@ -61,10 +58,9 @@ public BibtexDatabaseWriter(Writer writer, BibEntryTypesManager entryTypesManager) { super(new BibWriter(writer, newline), saveConfiguration, + fieldPreferences, citationKeyPatternPreferences, entryTypesManager); - - this.fieldPreferences = fieldPreferences; } @Override diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java new file mode 100644 index 00000000000..bda0735ebfe --- /dev/null +++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java @@ -0,0 +1,62 @@ +package org.jabref.logic.formatter.bibtexfields; + +import java.util.Objects; +import java.util.regex.Pattern; + +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +/** + * Replaces two subsequent whitespaces (and tabs) to one space in case of single-line fields. In case of multine fields, + * the field content is kept as is. + * + * Due to the distinction between single line and multiline fields, this formatter does not implement the interface {@link org.jabref.logic.cleanup.Formatter}. + */ +public class NormalizeWhitespaceFormatter { + + // 's' matches a space, tab, new line, carriage return. + private static final Pattern WHITESPACE = Pattern.compile("\\s+"); + + private final FieldPreferences preferences; + + public NormalizeWhitespaceFormatter(FieldPreferences preferences) { + Objects.requireNonNull(preferences); + this.preferences = preferences; + } + + /** + * Performs the reformatting of a field content. Note that "field content" is either with enclosing {}. + * When outputting something which is using strings, the parts of the plain string are passed (without enclosing {}). + * For instance, for #kopp# and #breit#", and is passed. + * Also depends on the caller whether strings have been resolved. + * + * @param fieldContent the content to format. + * @param field the name of the bibtex field + * @return the formatted field content. + */ + public String format(String fieldContent, Field field) { + if (FieldFactory.isMultiLineField(field, preferences.getNonWrappableFields())) { + // In general, keep the field as is. + // Newlines are normalized at org.jabref.logic.exporter.BibWriter + // Alternative: StringUtil.unifyLineBreaks(fieldContent, OS.NEWLINE) + return fieldContent; + } + + // Replace multiple whitespaces by one. We need to keep the leading and trailing whitespace to enable constructs such as "#kopp# and #breit#" + return WHITESPACE.matcher(fieldContent).replaceAll(" "); + } + + /** + * Performs the reformatting of a field content. Note that "field content" is understood as + * the value in BibTeX's key/value pairs of content. For instance, {author} is passed as + * content. This allows for things like jan { - } feb to be passed. + * + * @param fieldContent the content to format. + * @param field the name of the bibtex field + * @return the formatted field content. + */ + public String format(StringBuilder fieldContent, Field field) { + return format(fieldContent.toString(), field); + } +} diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/TrimWhitespaceFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/TrimWhitespaceFormatter.java index 8f80fdfaeb5..4e4d87788f0 100644 --- a/src/main/java/org/jabref/logic/formatter/bibtexfields/TrimWhitespaceFormatter.java +++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/TrimWhitespaceFormatter.java @@ -5,11 +5,16 @@ import org.jabref.logic.cleanup.Formatter; import org.jabref.logic.l10n.Localization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Trim all whitespace characters (as defined in Java) in the beginning and at the end of the string. */ public class TrimWhitespaceFormatter extends Formatter { + private static final Logger LOGGER = LoggerFactory.getLogger(TrimWhitespaceFormatter.class); + @Override public String getName() { return Localization.lang("Trim whitespace characters"); @@ -23,7 +28,9 @@ public String getKey() { @Override public String format(String value) { Objects.requireNonNull(value); - return value.trim(); + String result = value.trim(); + LOGGER.trace("Formatted '{}' to '{}'", value, result); + return result; } @Override diff --git a/src/main/java/org/jabref/logic/importer/EntryBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/EntryBasedParserFetcher.java index 0adef8aca44..28795882d36 100644 --- a/src/main/java/org/jabref/logic/importer/EntryBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/EntryBasedParserFetcher.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Objects; -import org.jabref.logic.cleanup.Formatter; import org.jabref.model.entry.BibEntry; /** @@ -19,7 +18,7 @@ * 2. Parse the response to get a list of {@link BibEntry} * 3. Post-process fetched entries */ -public interface EntryBasedParserFetcher extends EntryBasedFetcher { +public interface EntryBasedParserFetcher extends EntryBasedFetcher, ParserFetcher { /** * Constructs a URL based on the {@link BibEntry}. @@ -33,24 +32,6 @@ public interface EntryBasedParserFetcher extends EntryBasedFetcher { */ Parser getParser(); - /** - * Performs a cleanup of the fetched entry. - * - * Only systematic errors of the fetcher should be corrected here - * (i.e. if information is consistently contained in the wrong field or the wrong format) - * but not cosmetic issues which may depend on the user's taste (for example, LateX code vs HTML in the abstract). - * - * Try to reuse existing {@link Formatter} for the cleanup. For example, - * {@code new FieldFormatterCleanup(StandardField.TITLE, new RemoveBracesFormatter()).cleanup(entry);} - * - * By default, no cleanup is done. - * - * @param entry the entry to be cleaned-up - */ - default void doPostCleanup(BibEntry entry) { - // Do nothing by default - } - @Override default List performSearch(BibEntry entry) throws FetcherException { Objects.requireNonNull(entry); diff --git a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java index 1de7cc19a57..a938f8ef6f5 100644 --- a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.Optional; -import org.jabref.logic.cleanup.Formatter; import org.jabref.model.entry.BibEntry; import org.jabref.model.strings.StringUtil; @@ -21,7 +20,7 @@ * 2. Parse the response to get a list of {@link BibEntry} * 3. Post-process fetched entries */ -public interface IdBasedParserFetcher extends IdBasedFetcher { +public interface IdBasedParserFetcher extends IdBasedFetcher, ParserFetcher { Logger LOGGER = LoggerFactory.getLogger(IdBasedParserFetcher.class); @@ -37,24 +36,6 @@ public interface IdBasedParserFetcher extends IdBasedFetcher { */ Parser getParser(); - /** - * Performs a cleanup of the fetched entry. - * - * Only systematic errors of the fetcher should be corrected here - * (i.e. if information is consistently contained in the wrong field or the wrong format) - * but not cosmetic issues which may depend on the user's taste (for example, LateX code vs HTML in the abstract). - * - * Try to reuse existing {@link Formatter} for the cleanup. For example, - * {@code new FieldFormatterCleanup(StandardField.TITLE, new RemoveBracesFormatter()).cleanup(entry);} - * - * By default, no cleanup is done. - * - * @param entry the entry to be cleaned-up - */ - default void doPostCleanup(BibEntry entry) { - // Do nothing by default - } - @Override default Optional performSearchById(String identifier) throws FetcherException { if (StringUtil.isBlank(identifier)) { diff --git a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java index b9b5ed92e44..5907d81d1a3 100644 --- a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java @@ -11,7 +11,6 @@ import java.util.Objects; import java.util.Optional; -import org.jabref.logic.cleanup.Formatter; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.identifier.Identifier; @@ -24,7 +23,7 @@ * 2. Parse the response to get a list of {@link BibEntry} * 3. Extract identifier */ -public interface IdParserFetcher extends IdFetcher { +public interface IdParserFetcher extends IdFetcher, ParserFetcher { Logger LOGGER = LoggerFactory.getLogger(IdParserFetcher.class); @@ -40,24 +39,6 @@ public interface IdParserFetcher extends IdFetcher { */ Parser getParser(); - /** - * Performs a cleanup of the fetched entry. - * - * Only systematic errors of the fetcher should be corrected here - * (i.e. if information is consistently contained in the wrong field or the wrong format) - * but not cosmetic issues which may depend on the user's taste (for example, LateX code vs HTML in the abstract). - * - * Try to reuse existing {@link Formatter} for the cleanup. For example, - * {@code new FieldFormatterCleanup(StandardField.TITLE, new RemoveBracesFormatter()).cleanup(entry);} - * - * By default, no cleanup is done. - * - * @param entry the entry to be cleaned-up - */ - default void doPostCleanup(BibEntry entry) { - // Do nothing by default - } - /** * Extracts the identifier from the list of fetched entries. * diff --git a/src/main/java/org/jabref/logic/importer/ImportCleanup.java b/src/main/java/org/jabref/logic/importer/ImportCleanup.java index c6af4d0a2cd..8ba167f59b1 100644 --- a/src/main/java/org/jabref/logic/importer/ImportCleanup.java +++ b/src/main/java/org/jabref/logic/importer/ImportCleanup.java @@ -2,24 +2,46 @@ import java.util.Collection; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.cleanup.NormalizeWhitespacesCleanup; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; -public interface ImportCleanup { +import org.jspecify.annotations.NonNull; - static ImportCleanup targeting(BibDatabaseMode mode) { +/** + * Cleanup of imported entries to be processable by JabRef + */ +public abstract class ImportCleanup { + + private final NormalizeWhitespacesCleanup normalizeWhitespacesCleanup; + + protected ImportCleanup(FieldPreferences fieldPreferences) { + this.normalizeWhitespacesCleanup = new NormalizeWhitespacesCleanup(fieldPreferences); + } + + /** + * Kind of builder for a cleanup + */ + public static ImportCleanup targeting(BibDatabaseMode mode, @NonNull FieldPreferences fieldPreferences) { return switch (mode) { - case BIBTEX -> new ImportCleanupBibtex(); - case BIBLATEX -> new ImportCleanupBiblatex(); + case BIBTEX -> new ImportCleanupBibtex(fieldPreferences); + case BIBLATEX -> new ImportCleanupBiblatex(fieldPreferences); }; } - BibEntry doPostCleanup(BibEntry entry); + /** + * @implNote Related method: {@link ParserFetcher#doPostCleanup(BibEntry)} + */ + public BibEntry doPostCleanup(BibEntry entry) { + normalizeWhitespacesCleanup.cleanup(entry); + return entry; + } /** * Performs a format conversion of the given entry collection into the targeted format. */ - default void doPostCleanup(Collection entries) { + public void doPostCleanup(Collection entries) { entries.forEach(this::doPostCleanup); } } diff --git a/src/main/java/org/jabref/logic/importer/ImportCleanupBiblatex.java b/src/main/java/org/jabref/logic/importer/ImportCleanupBiblatex.java index 99919a2d3f0..269b489212a 100644 --- a/src/main/java/org/jabref/logic/importer/ImportCleanupBiblatex.java +++ b/src/main/java/org/jabref/logic/importer/ImportCleanupBiblatex.java @@ -1,12 +1,17 @@ package org.jabref.logic.importer; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.cleanup.ConvertToBiblatexCleanup; import org.jabref.model.entry.BibEntry; -public class ImportCleanupBiblatex implements ImportCleanup { +public class ImportCleanupBiblatex extends ImportCleanup { private final ConvertToBiblatexCleanup convertToBiblatexCleanup = new ConvertToBiblatexCleanup(); + public ImportCleanupBiblatex(FieldPreferences fieldPreferences) { + super(fieldPreferences); + } + /** * Performs a format conversion of the given entry into the targeted format. * Modifies the given entry and also returns it to enable usage of doPostCleanup in streams. @@ -15,6 +20,7 @@ public class ImportCleanupBiblatex implements ImportCleanup { */ @Override public BibEntry doPostCleanup(BibEntry entry) { + entry = super.doPostCleanup(entry); convertToBiblatexCleanup.cleanup(entry); return entry; } diff --git a/src/main/java/org/jabref/logic/importer/ImportCleanupBibtex.java b/src/main/java/org/jabref/logic/importer/ImportCleanupBibtex.java index ddda1f73e73..a8348baac96 100644 --- a/src/main/java/org/jabref/logic/importer/ImportCleanupBibtex.java +++ b/src/main/java/org/jabref/logic/importer/ImportCleanupBibtex.java @@ -1,12 +1,17 @@ package org.jabref.logic.importer; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.cleanup.ConvertToBibtexCleanup; import org.jabref.model.entry.BibEntry; -public class ImportCleanupBibtex implements ImportCleanup { +public class ImportCleanupBibtex extends ImportCleanup { private final ConvertToBibtexCleanup convertToBibtexCleanup = new ConvertToBibtexCleanup(); + public ImportCleanupBibtex(FieldPreferences fieldPreferences) { + super(fieldPreferences); + } + /** * Performs a format conversion of the given entry into the targeted format. * Modifies the given entry and also returns it to enable usage of doPostCleanup in streams. @@ -15,6 +20,7 @@ public class ImportCleanupBibtex implements ImportCleanup { */ @Override public BibEntry doPostCleanup(BibEntry entry) { + entry = super.doPostCleanup(entry); convertToBibtexCleanup.cleanup(entry); return entry; } diff --git a/src/main/java/org/jabref/logic/importer/PagedSearchBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/PagedSearchBasedParserFetcher.java index 9f08d042c69..5c2fa3d3dbb 100644 --- a/src/main/java/org/jabref/logic/importer/PagedSearchBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/PagedSearchBasedParserFetcher.java @@ -12,7 +12,7 @@ import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; -public interface PagedSearchBasedParserFetcher extends SearchBasedParserFetcher, PagedSearchBasedFetcher { +public interface PagedSearchBasedParserFetcher extends SearchBasedParserFetcher, PagedSearchBasedFetcher, ParserFetcher { @Override default Page performSearchPaged(QueryNode luceneQuery, int pageNumber) throws FetcherException { diff --git a/src/main/java/org/jabref/logic/importer/ParserFetcher.java b/src/main/java/org/jabref/logic/importer/ParserFetcher.java new file mode 100644 index 00000000000..847eea92e20 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/ParserFetcher.java @@ -0,0 +1,25 @@ +package org.jabref.logic.importer; + +import org.jabref.logic.cleanup.Formatter; +import org.jabref.model.entry.BibEntry; + +public interface ParserFetcher { + + /** + * Performs a cleanup of the fetched entry. + *

+ * Only systematic errors of the fetcher should be corrected here + * (i.e. if information is consistently contained in the wrong field or the wrong format) + * but not cosmetic issues which may depend on the user's taste (for example, LateX code vs HTML in the abstract). + *

+ * Try to reuse existing {@link Formatter} for the cleanup. For example, + * {@code new FieldFormatterCleanup(StandardField.TITLE, new RemoveBracesFormatter()).cleanup(entry);} + *

+ * By default, no cleanup is done. + * + * @param entry the entry to be cleaned-up + */ + default void doPostCleanup(BibEntry entry) { + // Do nothing by default + } +} diff --git a/src/main/java/org/jabref/logic/importer/SearchBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/SearchBasedParserFetcher.java index 1d9c2882acf..16f8a33598a 100644 --- a/src/main/java/org/jabref/logic/importer/SearchBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/SearchBasedParserFetcher.java @@ -7,7 +7,6 @@ import java.net.URL; import java.util.List; -import org.jabref.logic.cleanup.Formatter; import org.jabref.model.entry.BibEntry; import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; @@ -33,7 +32,7 @@ * We need multi inheritance, because a fetcher might implement multiple query types (such as id fetching {@link IdBasedFetcher}), complete entry {@link EntryBasedFetcher}, and search-based fetcher (this class). *

*/ -public interface SearchBasedParserFetcher extends SearchBasedFetcher { +public interface SearchBasedParserFetcher extends SearchBasedFetcher, ParserFetcher { /** * This method is used to send queries with advanced URL parameters. @@ -77,22 +76,4 @@ private List getBibEntries(URL urlForQuery) throws FetcherException { * @param luceneQuery the root node of the lucene query */ URL getURLForQuery(QueryNode luceneQuery) throws URISyntaxException, MalformedURLException, FetcherException; - - /** - * Performs a cleanup of the fetched entry. - *

- * Only systematic errors of the fetcher should be corrected here - * (i.e. if information is consistently contained in the wrong field or the wrong format) - * but not cosmetic issues which may depend on the user's taste (for example, LateX code vs HTML in the abstract). - *

- * Try to reuse existing {@link Formatter} for the cleanup. For example, - * {@code new FieldFormatterCleanup(StandardField.TITLE, new RemoveBracesFormatter()).cleanup(entry);} - *

- * By default, no cleanup is done. - * - * @param entry the entry to be cleaned-up - */ - default void doPostCleanup(BibEntry entry) { - // Do nothing by default - } } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/MathSciNet.java b/src/main/java/org/jabref/logic/importer/fetcher/MathSciNet.java index 6c69203a0a4..1a63680e589 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/MathSciNet.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/MathSciNet.java @@ -46,7 +46,7 @@ import org.slf4j.LoggerFactory; /** - * Fetches data from the MathSciNet (http://www.ams.org/mathscinet) + * Fetches data from the MathSciNet API. */ public class MathSciNet implements SearchBasedParserFetcher, EntryBasedParserFetcher, IdBasedParserFetcher { private static final Logger LOGGER = LoggerFactory.getLogger(MathSciNet.class); @@ -73,7 +73,7 @@ public String getName() { } /** - * We use MR Lookup (https://mathscinet.ams.org/mathscinet/freetools/mrlookup) instead of the usual search since this tool is also available + * We use MR Lookup instead of the usual search since this tool is also available * without subscription and, moreover, is optimized for finding a publication based on partial information. */ @Override @@ -180,18 +180,17 @@ private BibEntry jsonItemToBibEntry(JSONObject item) throws ParseException { } // Handle articleUrl and mrnumber fields separately, as they are non-nested properties in the JSON and can be retrieved as Strings directly - String doi = item.optString("articleUrl", ""); + String doi = item.optString("articleUrl"); if (!doi.isEmpty()) { try { - Optional parsedDoi = DOI.parse(doi); - parsedDoi.ifPresent(validDoi -> entry.setField(StandardField.DOI, validDoi.getNormalized())); + DOI.parse(doi).ifPresent(validDoi -> entry.setField(StandardField.DOI, validDoi.getNormalized())); } catch (IllegalArgumentException e) { // If DOI parsing fails, use the original DOI string entry.setField(StandardField.DOI, doi); } } - String mrNumber = item.optString("mrnumber", ""); + String mrNumber = item.optString("mrnumber"); if (!mrNumber.isEmpty()) { entry.setField(StandardField.MR_NUMBER, mrNumber); } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index c1edfec27d8..72b07d625dd 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -25,7 +25,6 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import org.jabref.logic.bibtex.FieldContentFormatter; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.exporter.BibtexDatabaseWriter; import org.jabref.logic.exporter.SaveConfiguration; @@ -93,7 +92,6 @@ public class BibtexParser implements Parser { private static final Integer LOOKAHEAD = 1024; private static final String BIB_DESK_ROOT_GROUP_NAME = "BibDeskGroups"; private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); - private final FieldContentFormatter fieldContentFormatter; private final Deque pureTextFromFile = new LinkedList<>(); private final ImportFormatPreferences importFormatPreferences; private PushbackReader pushbackReader; @@ -109,7 +107,6 @@ public class BibtexParser implements Parser { public BibtexParser(ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor) { this.importFormatPreferences = Objects.requireNonNull(importFormatPreferences); - this.fieldContentFormatter = new FieldContentFormatter(importFormatPreferences.fieldPreferences()); this.metaDataParser = new MetaDataParser(fileMonitor); this.parsedBibdeskGroups = new HashMap<>(); } @@ -728,18 +725,15 @@ private void parseField(BibEntry entry) throws IOException { if (!content.isEmpty()) { if (entry.hasField(field)) { // The following hack enables the parser to deal with multiple - // author or - // editor lines, stringing them together instead of getting just + // author or editor lines, stringing them together instead of getting just // one of them. // Multiple author or editor lines are not allowed by the bibtex - // format, but - // at least one online database exports bibtex likes to do that, making - // it inconvenient - // for users if JabRef did not accept it. + // format, but at least one online database exports bibtex likes to do that, making + // it inconvenient for users if JabRef did not accept it. if (field.getProperties().contains(FieldProperty.PERSON_NAMES)) { entry.setField(field, entry.getField(field).orElse("") + " and " + content); } else if (StandardField.KEYWORDS == field) { - // multiple keywords fields should be combined to one + // TODO: multiple keywords fields should be combined to one entry.addKeyword(content, importFormatPreferences.bibEntryPreferences().getKeywordSeparator()); } } else { @@ -781,13 +775,13 @@ private String parseFieldContent(Field field) throws IOException { } if (character == '"') { StringBuilder text = parseQuotedFieldExactly(); - value.append(fieldContentFormatter.format(text, field)); + value.append(text.toString()); } else if (character == '{') { // Value is a string enclosed in brackets. There can be pairs // of brackets inside a field, so we need to count the // brackets to know when the string is finished. StringBuilder text = parseBracketedFieldContent(); - value.append(fieldContentFormatter.format(text, field)); + value.append(text.toString()); } else if (Character.isDigit((char) character)) { // value is a number String number = parseTextToken(); value.append(number); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MedlinePlainImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MedlinePlainImporter.java index 2acd1b175d7..1a10817f0d1 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MedlinePlainImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MedlinePlainImporter.java @@ -341,7 +341,7 @@ private void addAbstract(Map hm, String lab, String value) { // remove the copyright from the field since the name of the field is copyright String copyrightInfo = value.substring(copyrightIndex).replace("Copyright ", ""); hm.put(new UnknownField("copyright"), copyrightInfo); - abstractValue = value.substring(0, copyrightIndex); + abstractValue = value.substring(0, copyrightIndex).trim(); } else { abstractValue = value; } diff --git a/src/main/java/org/jabref/logic/integrity/AmpersandChecker.java b/src/main/java/org/jabref/logic/integrity/AmpersandChecker.java index ac8f4cb6748..43098173dfa 100644 --- a/src/main/java/org/jabref/logic/integrity/AmpersandChecker.java +++ b/src/main/java/org/jabref/logic/integrity/AmpersandChecker.java @@ -11,7 +11,7 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldProperty; +import org.jabref.model.entry.field.FieldFactory; import com.google.common.base.CharMatcher; @@ -28,7 +28,7 @@ public class AmpersandChecker implements EntryChecker { @Override public List check(BibEntry entry) { return entry.getFieldMap().entrySet().stream() - .filter(field -> !field.getKey().getProperties().contains(FieldProperty.VERBATIM)) + .filter(field -> FieldFactory.isLatexField(field.getKey())) // We use "flatMap" instead of filtering later, because we assume there won't be that much error messages - and construction of Stream.empty() is faster than construction of a new Tuple2 (including lifting long to Long) .flatMap(AmpersandChecker::getUnescapedAmpersandsWithCount) .map(pair -> new IntegrityMessage(Localization.lang("Found %0 unescaped '&'", pair.getValue()), entry, pair.getKey())) diff --git a/src/main/java/org/jabref/logic/integrity/BibStringChecker.java b/src/main/java/org/jabref/logic/integrity/BibStringChecker.java index 567094d868e..6581e929658 100644 --- a/src/main/java/org/jabref/logic/integrity/BibStringChecker.java +++ b/src/main/java/org/jabref/logic/integrity/BibStringChecker.java @@ -10,7 +10,7 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldProperty; +import org.jabref.model.entry.field.FieldFactory; /** * Checks, if there is an even number of unescaped # (FieldWriter.BIBTEX_STRING_START_END_SYMBOL) @@ -27,7 +27,7 @@ public List check(BibEntry entry) { Map fields = entry.getFieldMap(); for (Map.Entry field : fields.entrySet()) { - if (!field.getKey().getProperties().contains(FieldProperty.VERBATIM)) { + if (FieldFactory.isLatexField(field.getKey())) { Matcher hashMatcher = UNESCAPED_HASH.matcher(field.getValue()); int hashCount = 0; while (hashMatcher.find()) { diff --git a/src/main/java/org/jabref/logic/integrity/HTMLCharacterChecker.java b/src/main/java/org/jabref/logic/integrity/HTMLCharacterChecker.java index 874a9c79c8d..b987a1f541a 100644 --- a/src/main/java/org/jabref/logic/integrity/HTMLCharacterChecker.java +++ b/src/main/java/org/jabref/logic/integrity/HTMLCharacterChecker.java @@ -8,7 +8,7 @@ import org.jabref.model.entry.field.FieldProperty; /** - * Checks, if there are any HTML encoded characters in nonverbatim fields. + * Checks, if there are any HTML encoded characters in non-verbatim fields. */ public class HTMLCharacterChecker implements EntryChecker { // Detect any HTML encoded character diff --git a/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java b/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java index 44f7361aa36..b12dfadbf04 100644 --- a/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java +++ b/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java @@ -14,7 +14,7 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldProperty; +import org.jabref.model.entry.field.FieldFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,7 +62,7 @@ public class LatexIntegrityChecker implements EntryChecker { @Override public List check(BibEntry entry) { return entry.getFieldMap().entrySet().stream() - .filter(field -> !field.getKey().getProperties().contains(FieldProperty.VERBATIM)) + .filter(field -> FieldFactory.isLatexField(field.getKey())) .flatMap(LatexIntegrityChecker::getUnescapedAmpersandsWithCount) // Exclude all DOM building errors as this functionality is not used. .filter(pair -> !pair.getValue().getErrorCode().getErrorGroup().equals(CoreErrorGroup.TDE)) diff --git a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java index d75b90aaf45..020b4c8de9d 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java +++ b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; import org.jabref.logic.exporter.BibDatabaseWriter; import org.jabref.logic.exporter.MetaDataSerializer; @@ -53,14 +54,17 @@ public class DBMSSynchronizer implements DatabaseSynchronizer { private Connection currentConnection; private final Character keywordSeparator; private final GlobalCitationKeyPatterns globalCiteKeyPattern; + private final FieldPreferences fieldPreferences; private final FileUpdateMonitor fileMonitor; private Optional lastEntryChanged; public DBMSSynchronizer(BibDatabaseContext bibDatabaseContext, Character keywordSeparator, + FieldPreferences fieldPreferences, GlobalCitationKeyPatterns globalCiteKeyPattern, FileUpdateMonitor fileMonitor) { this.bibDatabaseContext = Objects.requireNonNull(bibDatabaseContext); this.bibDatabase = bibDatabaseContext.getDatabase(); this.metaData = bibDatabaseContext.getMetaData(); + this.fieldPreferences = fieldPreferences; this.fileMonitor = fileMonitor; this.eventBus = new EventBus(); this.keywordSeparator = keywordSeparator; @@ -243,7 +247,7 @@ public void synchronizeSharedEntry(BibEntry bibEntry) { return; } try { - BibDatabaseWriter.applySaveActions(bibEntry, metaData); // perform possibly existing save actions + BibDatabaseWriter.applySaveActions(bibEntry, metaData, fieldPreferences); // perform possibly existing save actions dbmsProcessor.updateEntry(bibEntry); } catch (OfflineLockException exception) { eventBus.post(new UpdateRefusedEvent(bibDatabaseContext, exception.getLocalBibEntry(), exception.getSharedBibEntry())); @@ -294,7 +298,7 @@ public void applyMetaData() { for (BibEntry bibEntry : bibDatabase.getEntries()) { try { // synchronize only if changes were present - if (!BibDatabaseWriter.applySaveActions(bibEntry, metaData).isEmpty()) { + if (!BibDatabaseWriter.applySaveActions(bibEntry, metaData, fieldPreferences).isEmpty()) { dbmsProcessor.updateEntry(bibEntry); } } catch (OfflineLockException exception) { diff --git a/src/main/java/org/jabref/model/database/KeyChangeListener.java b/src/main/java/org/jabref/model/database/KeyChangeListener.java index d65b2764414..6dc601971f1 100644 --- a/src/main/java/org/jabref/model/database/KeyChangeListener.java +++ b/src/main/java/org/jabref/model/database/KeyChangeListener.java @@ -9,7 +9,6 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.event.FieldChangedEvent; 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; @@ -43,15 +42,16 @@ public void listen(EntriesRemovedEvent event) { private void updateEntryLinks(String newKey, String oldKey) { for (BibEntry entry : database.getEntries()) { - for (Field field : FieldFactory.getKeyFields()) { - entry.getField(field).ifPresent(fieldContent -> { - if (field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK)) { - replaceSingleKeyInField(newKey, oldKey, entry, field, fieldContent); - } else { // MULTIPLE_ENTRY_LINK - replaceKeyInMultiplesKeyField(newKey, oldKey, entry, field, fieldContent); - } - }); - } + entry.getFields(field -> field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK)) + .forEach(field -> { + String fieldContent = entry.getField(field).orElseThrow(); + replaceSingleKeyInField(newKey, oldKey, entry, field, fieldContent); + }); + entry.getFields(field -> field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK)) + .forEach(field -> { + String fieldContent = entry.getField(field).orElseThrow(); + replaceKeyInMultiplesKeyField(newKey, oldKey, entry, field, fieldContent); + }); } } diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index b52b827579f..04566a98c5e 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -16,6 +16,7 @@ import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; +import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.beans.Observable; @@ -457,6 +458,13 @@ public SequencedSet getFields() { return new LinkedHashSet<>(fields.keySet()); } + /** + * Returns an unmodifiable sequence containing the names of all fields that are a) set for this particular entry and b) matching the given predicate + */ + public SequencedSet getFields(Predicate selector) { + return getFields().stream().filter(selector).collect(Collectors.toCollection(LinkedHashSet::new)); + } + /** * Returns the contents of the given field as an Optional. */ @@ -695,6 +703,7 @@ public boolean allFieldsPresent(Collection fields, BibDatabase databas /** * Returns a clone of this entry. Useful for copying. * This will set a new ID for the cloned entry to be able to distinguish both copies. + * Does not port the listeners. */ @Override public Object clone() { diff --git a/src/main/java/org/jabref/model/entry/Month.java b/src/main/java/org/jabref/model/entry/Month.java index 2f76291a431..26bec5fd86a 100644 --- a/src/main/java/org/jabref/model/entry/Month.java +++ b/src/main/java/org/jabref/model/entry/Month.java @@ -51,7 +51,7 @@ public static Optional getMonthByNumber(int number) { } /** - * Find month by shortName (3 letters) case insensitive. + * Find month by shortName (3 letters) case-insensitive. * If no matching month is found, then an empty Optional is returned. * * @param shortName "jan", "feb", ... diff --git a/src/main/java/org/jabref/model/entry/field/FieldFactory.java b/src/main/java/org/jabref/model/entry/field/FieldFactory.java index 5c0a9036d2b..08ae84ed852 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldFactory.java +++ b/src/main/java/org/jabref/model/entry/field/FieldFactory.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashSet; @@ -49,12 +50,25 @@ public static String serializeOrFieldsList(Set fields) { return fields.stream().map(FieldFactory::serializeOrFields).collect(Collectors.joining(DELIMITER)); } - public static List getNotTextFieldNames() { - return Arrays.asList(StandardField.DOI, StandardField.FILE, StandardField.URL, StandardField.URI, StandardField.ISBN, StandardField.ISSN, StandardField.MONTH, StandardField.DATE, StandardField.YEAR); + /** + * Checks whether the given field contains LaTeX code or something else + */ + public static boolean isLatexField(Field field) { + return Collections.disjoint(field.getProperties(), Set.of(FieldProperty.VERBATIM, FieldProperty.MARKDOWN)); } - public static List getIdentifierFieldNames() { - return Arrays.asList(StandardField.DOI, StandardField.EPRINT, StandardField.PMID); + /** + * Returns a collection of StandardFields where the content should not be interpreted as "plain" text, but something else (such as links to other fields, numbers, ...) + */ + public static Collection getNotTextFields() { + Set result = Arrays.stream(StandardField.values()) + .filter(field -> !Collections.disjoint(field.getProperties(), Set.of(FieldProperty.VERBATIM, FieldProperty.NUMERIC, FieldProperty.DATE, FieldProperty.MULTIPLE_ENTRY_LINK))) + .collect(Collectors.toSet()); + + // These fields are not marked as verbatim, because they could include LaTeX code + result.add(StandardField.MONTH); + result.add(StandardField.DATE); + return result; } public static OrFields parseOrFields(String fieldNames) { @@ -123,10 +137,6 @@ public static Field parseField(String fieldName) { return parseField(null, fieldName); } - public static Set getKeyFields() { - return getFieldsFiltered(field -> field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK) || field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK)); - } - public static boolean isInternalField(Field field) { return field.getName().startsWith("__"); } @@ -162,9 +172,9 @@ public static Set getAllFieldsWithOutInternal() { } /** - * Returns a List with all standard fields and the citation key field + * Returns a list with all standard fields and the citation key field */ - public static Set getStandardFieldsWithCitationKey() { + public static SequencedSet getStandardFieldsWithCitationKey() { EnumSet allFields = EnumSet.allOf(StandardField.class); LinkedHashSet standardFieldsWithBibtexKey = new LinkedHashSet<>(allFields.size() + 1); @@ -196,7 +206,6 @@ private static Set getAllFields() { fields.addAll(EnumSet.allOf(InternalField.class)); fields.addAll(EnumSet.allOf(SpecialField.class)); fields.addAll(EnumSet.allOf(StandardField.class)); - fields.removeIf(field -> field instanceof UserSpecificCommentField); return fields; } diff --git a/src/main/java/org/jabref/model/entry/field/FieldProperty.java b/src/main/java/org/jabref/model/entry/field/FieldProperty.java index bf9bc58dd6d..cd8cc753980 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldProperty.java +++ b/src/main/java/org/jabref/model/entry/field/FieldProperty.java @@ -1,31 +1,51 @@ package org.jabref.model.entry.field; +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.autocompleter.SuggestionProviders; +import org.jabref.gui.fieldeditors.FieldEditors; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.journals.JournalAbbreviationRepository; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.types.EntryType; +import org.jabref.preferences.PreferencesService; + +/** + * @implNote Introduce a new FieldProperty only if multiple fields with the same property exist. + * For instance, "gender" exists only in field "Gender", whereas "identifier" is the property of multiple fields. + * It is confusing to have a FieldProperty for a single field. + * We accept that some developers might be confused for different handling at {@link FieldEditors#getForField(Field, TaskExecutor, DialogService, JournalAbbreviationRepository, PreferencesService, BibDatabaseContext, EntryType, SuggestionProviders, UndoManager)}. + */ public enum FieldProperty { BOOK_NAME, DATE, - DOI, EDITOR_TYPE, - EPRINT, EXTERNAL, FILE_EDITOR, - GENDER, - ISBN, - ISSN, JOURNAL_NAME, + + // globally unique identifier for the concrete article + IDENTIFIER, + LANGUAGE, - MARKDOWN, // Field content is text, but should be interpreted as markdown + + // Field content is text, but should be interpreted as markdown + // AKA: Field content is not LaTeX + MARKDOWN, + MONTH, - MULTIPLE_ENTRY_LINK, + MULTILINE_TEXT, NUMERIC, - PAGES, PAGINATION, PERSON_NAMES, - PUBLICATION_STATE, + SINGLE_ENTRY_LINK, - TYPE, + MULTIPLE_ENTRY_LINK, + + // Field content should be treated as data VERBATIM, - YES_NO, - COMMENT, - CUSTOM_FIELD + + YES_NO } diff --git a/src/main/java/org/jabref/model/entry/field/InternalField.java b/src/main/java/org/jabref/model/entry/field/InternalField.java index 6dd607e6aae..427f86483c5 100644 --- a/src/main/java/org/jabref/model/entry/field/InternalField.java +++ b/src/main/java/org/jabref/model/entry/field/InternalField.java @@ -15,8 +15,8 @@ public enum InternalField implements Field { /** * field which indicates the entrytype - * - * Example: @misc{key} + *

+ * Example: @misc{key} */ TYPE_HEADER("entrytype"), diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index befb6b84a63..776e5019054 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -33,13 +33,13 @@ public enum StandardField implements Field { CHAPTER("chapter"), COMMENTATOR("commentator", FieldProperty.PERSON_NAMES), // Comments of users are handled at {@link org.jabref.model.entry.field.UserSpecificCommentField} - COMMENT("comment", FieldProperty.COMMENT, FieldProperty.MULTILINE_TEXT, FieldProperty.VERBATIM, FieldProperty.MARKDOWN), + COMMENT("comment", FieldProperty.MULTILINE_TEXT, FieldProperty.MARKDOWN), CROSSREF("crossref", FieldProperty.SINGLE_ENTRY_LINK), CITES("cites", FieldProperty.MULTIPLE_ENTRY_LINK), DATE("date", FieldProperty.DATE), DAY("day"), DAYFILED("dayfiled"), - DOI("doi", "DOI", FieldProperty.DOI, FieldProperty.VERBATIM), + DOI("doi", "DOI", FieldProperty.VERBATIM, FieldProperty.IDENTIFIER), EDITION("edition", FieldProperty.NUMERIC), EDITOR("editor", FieldProperty.PERSON_NAMES), EDITORA("editora", FieldProperty.PERSON_NAMES), @@ -51,24 +51,24 @@ public enum StandardField implements Field { EDITORCTYPE("editorctype", FieldProperty.EDITOR_TYPE), EID("eid"), ENTRYSET("entryset", FieldProperty.MULTIPLE_ENTRY_LINK), - EPRINT("eprint", FieldProperty.EPRINT, FieldProperty.VERBATIM), + EPRINT("eprint", FieldProperty.VERBATIM, FieldProperty.IDENTIFIER), EPRINTCLASS("eprintclass"), EPRINTTYPE("eprinttype"), EVENTDATE("eventdate", FieldProperty.DATE), EVENTTITLE("eventtitle"), EVENTTITLEADDON("eventtitleaddon"), - FILE("file", FieldProperty.FILE_EDITOR, FieldProperty.VERBATIM), + FILE("file", FieldProperty.VERBATIM), FOREWORD("foreword", FieldProperty.PERSON_NAMES), FOLDER("folder"), - GENDER("gender", FieldProperty.GENDER), + GENDER("gender"), HOLDER("holder", FieldProperty.PERSON_NAMES), HOWPUBLISHED("howpublished"), IDS("ids", FieldProperty.MULTIPLE_ENTRY_LINK), INSTITUTION("institution"), INTRODUCTION("introduction", FieldProperty.PERSON_NAMES), - ISBN("isbn", "ISBN", FieldProperty.ISBN, FieldProperty.VERBATIM), + ISBN("isbn", "ISBN", FieldProperty.VERBATIM), ISRN("isrn", "ISRN", FieldProperty.VERBATIM), - ISSN("issn", "ISSN", FieldProperty.ISSN, FieldProperty.VERBATIM), + ISSN("issn", "ISSN", FieldProperty.VERBATIM), ISSUE("issue"), ISSUETITLE("issuetitle"), ISSUESUBTITLE("issuesubtitle"), @@ -94,15 +94,15 @@ public enum StandardField implements Field { ORGANIZATION("organization"), ORIGDATE("origdate", FieldProperty.DATE), ORIGLANGUAGE("origlanguage", FieldProperty.LANGUAGE), - PAGES("pages", FieldProperty.PAGES), + PAGES("pages"), PAGETOTAL("pagetotal"), PAGINATION("pagination", FieldProperty.PAGINATION), PART("part"), PDF("pdf", "PDF"), - PMID("pmid", "PMID", FieldProperty.NUMERIC), + PMID("pmid", "PMID", FieldProperty.NUMERIC, FieldProperty.IDENTIFIER), PS("ps", "PS"), PUBLISHER("publisher"), - PUBSTATE("pubstate", FieldProperty.PUBLICATION_STATE), + PUBSTATE("pubstate"), PRIMARYCLASS("primaryclass"), RELATED("related", FieldProperty.MULTIPLE_ENTRY_LINK), REPORTNO("reportno"), @@ -119,7 +119,7 @@ public enum StandardField implements Field { TITLE("title"), TITLEADDON("titleaddon"), TRANSLATOR("translator", FieldProperty.PERSON_NAMES), - TYPE("type", FieldProperty.TYPE), + TYPE("type"), URI("uri", "URI", FieldProperty.EXTERNAL, FieldProperty.VERBATIM), URL("url", "URL", FieldProperty.EXTERNAL, FieldProperty.VERBATIM), URLDATE("urldate", FieldProperty.DATE), diff --git a/src/main/java/org/jabref/model/entry/field/UnknownField.java b/src/main/java/org/jabref/model/entry/field/UnknownField.java index 3fee64991c1..5e4ff7bc39d 100644 --- a/src/main/java/org/jabref/model/entry/field/UnknownField.java +++ b/src/main/java/org/jabref/model/entry/field/UnknownField.java @@ -32,7 +32,7 @@ public UnknownField(String name, String displayName, FieldProperty first, FieldP } public static UnknownField fromDisplayName(String displayName) { - return new UnknownField(displayName.toLowerCase(Locale.ROOT), displayName, FieldProperty.CUSTOM_FIELD); + return new UnknownField(displayName.toLowerCase(Locale.ROOT), displayName); } @Override diff --git a/src/main/java/org/jabref/model/entry/field/UserSpecificCommentField.java b/src/main/java/org/jabref/model/entry/field/UserSpecificCommentField.java index 0ad27a5f70d..89e437e2e2f 100644 --- a/src/main/java/org/jabref/model/entry/field/UserSpecificCommentField.java +++ b/src/main/java/org/jabref/model/entry/field/UserSpecificCommentField.java @@ -4,7 +4,7 @@ import java.util.Objects; public class UserSpecificCommentField implements Field { - private static final EnumSet PROPERTIES = EnumSet.of(FieldProperty.COMMENT, FieldProperty.MULTILINE_TEXT, FieldProperty.VERBATIM, FieldProperty.MARKDOWN); + private static final EnumSet PROPERTIES = EnumSet.of(FieldProperty.MULTILINE_TEXT, FieldProperty.MARKDOWN); private final String name; public UserSpecificCommentField(String username) { diff --git a/src/test/java/org/jabref/cli/JabRefCLITest.java b/src/test/java/org/jabref/cli/JabRefCLITest.java index 8fcbbd01013..89d3bd43f7c 100644 --- a/src/test/java/org/jabref/cli/JabRefCLITest.java +++ b/src/test/java/org/jabref/cli/JabRefCLITest.java @@ -5,6 +5,8 @@ import javafx.util.Pair; +import org.jabref.logic.util.OS; + import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -146,7 +148,7 @@ void alignStringTable() { Bread : Loaf Paper : Sheet Country : County - """; + """.replace("\n", OS.NEWLINE); assertEquals(expected, JabRefCLI.alignStringTable(given)); } diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java index 973f36a43d4..29a8d46e14e 100644 --- a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java @@ -5,11 +5,14 @@ import javax.swing.undo.UndoManager; +import javafx.collections.FXCollections; + import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.util.CurrentThreadTaskExecutor; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; import org.jabref.logic.database.DuplicateCheck; @@ -62,6 +65,10 @@ void setUp() { when(importerPreferences.isGenerateNewKeyOnImport()).thenReturn(false); when(preferencesService.getImporterPreferences()).thenReturn(importerPreferences); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + when(preferencesService.getFieldPreferences()).thenReturn(fieldPreferences); + when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); when(preferencesService.getOwnerPreferences()).thenReturn(mock(OwnerPreferences.class, Answers.RETURNS_DEEP_STUBS)); when(preferencesService.getTimestampPreferences()).thenReturn(mock(TimestampPreferences.class, Answers.RETURNS_DEEP_STUBS)); diff --git a/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java b/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java index 9e826e1a0b4..bf69962cc37 100644 --- a/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java +++ b/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java @@ -4,10 +4,13 @@ import javax.swing.undo.UndoManager; +import javafx.collections.FXCollections; + import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.duplicationFinder.DuplicateResolverDialog; import org.jabref.gui.util.CurrentThreadTaskExecutor; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.model.database.BibDatabase; @@ -54,6 +57,10 @@ void setUp() { when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + when(preferencesService.getFieldPreferences()).thenReturn(fieldPreferences); + bibDatabaseContext = mock(BibDatabaseContext.class); BibDatabase bibDatabase = new BibDatabase(); when(bibDatabaseContext.getMode()).thenReturn(BibDatabaseMode.BIBTEX); diff --git a/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java b/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java index 9dbc53287e9..5d6edc46ed5 100644 --- a/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java +++ b/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java @@ -4,7 +4,6 @@ import java.io.StringReader; import java.io.StringWriter; import java.nio.file.Path; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; @@ -53,18 +52,18 @@ void setUpWriter() { } @Test - void serialization() throws IOException { - BibEntry entry = new BibEntry(StandardEntryType.Article); - // set a required field - entry.setField(StandardField.AUTHOR, "Foo Bar"); - entry.setField(StandardField.JOURNAL, "International Journal of Something"); - // set an optional field - entry.setField(StandardField.NUMBER, "1"); - entry.setField(StandardField.NOTE, "some note"); + void serialization() throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + // set required fields + .withField(StandardField.AUTHOR, "Foo Bar") + .withField(StandardField.JOURNAL, "International Journal of Something") + // set optional fields + .withField(StandardField.NUMBER, "1") + .withField(StandardField.NOTE, "some note") + .withChanged(true); bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); - // @formatter:off String expected = """ @Article{, author = {Foo Bar}, @@ -73,11 +72,40 @@ void serialization() throws IOException { number = {1}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected, stringWriter.toString()); } + @Test + void bibEntryTwoSpacesBeforeAndAfterKept() throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, " two spaces before and after (before) ") + .withChanged(true); + + bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); + + String expected = """ + @Article{, + author = { two spaces before and after (before) }, + } + """.replace("\n", OS.NEWLINE); + + assertEquals(expected, stringWriter.toString()); + } + + @Test + void bibEntryNotModified() throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, " two spaces before and after ") + .withChanged(true); + + BibEntry original = (BibEntry) entry.clone(); + + bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); + + assertEquals(original, entry); + } + @Test void writeOtherTypeTest() throws Exception { String expected = """ @@ -86,9 +114,10 @@ void writeOtherTypeTest() throws Exception { } """.replace("\n", OS.NEWLINE); - BibEntry entry = new BibEntry(new UnknownEntryType("other")); - entry.setField(StandardField.COMMENT, "testentry"); - entry.setCitationKey("test"); + BibEntry entry = new BibEntry(new UnknownEntryType("other")) + .withField(StandardField.COMMENT, "testentry") + .withCitationKey("test") + .withChanged(true); bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); assertEquals(expected, stringWriter.toString()); @@ -111,17 +140,17 @@ void writeEntryWithFile() throws Exception { @Test void writeEntryWithOrField() throws Exception { - BibEntry entry = new BibEntry(StandardEntryType.InBook); - // set an required OR field (author/editor) - entry.setField(StandardField.EDITOR, "Foo Bar"); - entry.setField(StandardField.JOURNAL, "International Journal of Something"); - // set an optional field - entry.setField(StandardField.NUMBER, "1"); - entry.setField(StandardField.NOTE, "some note"); + BibEntry entry = new BibEntry(StandardEntryType.InBook) + // set a required OR field (author/editor) + .withField(StandardField.EDITOR, "Foo Bar") + .withField(StandardField.JOURNAL, "International Journal of Something") + // set an optional field + .withField(StandardField.NUMBER, "1") + .withField(StandardField.NOTE, "some note") + .withChanged(true); bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); - // @formatter:off String expected = """ @InBook{, editor = {Foo Bar}, @@ -130,25 +159,24 @@ void writeEntryWithOrField() throws Exception { journal = {International Journal of Something}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected, stringWriter.toString()); } @Test void writeEntryWithOrFieldBothFieldsPresent() throws Exception { - BibEntry entry = new BibEntry(StandardEntryType.InBook); - // set an required OR field with both fields(author/editor) - entry.setField(StandardField.AUTHOR, "Foo Thor"); - entry.setField(StandardField.EDITOR, "Edi Bar"); - entry.setField(StandardField.JOURNAL, "International Journal of Something"); - // set an optional field - entry.setField(StandardField.NUMBER, "1"); - entry.setField(StandardField.NOTE, "some note"); + BibEntry entry = new BibEntry(StandardEntryType.InBook) + // set a required OR field with both fields(author/editor) + .withField(StandardField.AUTHOR, "Foo Thor") + .withField(StandardField.EDITOR, "Edi Bar") + .withField(StandardField.JOURNAL, "International Journal of Something") + // set an optional field + .withField(StandardField.NUMBER, "1") + .withField(StandardField.NOTE, "some note") + .withChanged(true); bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); - // @formatter:off String expected = """ @InBook{, author = {Foo Thor}, @@ -158,7 +186,6 @@ void writeEntryWithOrFieldBothFieldsPresent() throws Exception { journal = {International Journal of Something}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected, stringWriter.toString()); } @@ -193,8 +220,7 @@ void roundTripTest() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -212,8 +238,7 @@ void roundTripKeepsFilePathWithBackslashes() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -231,8 +256,7 @@ void roundTripKeepsEscapedCharacters() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -250,8 +274,7 @@ void roundTripKeepsFilePathEndingWithBackslash() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -272,8 +295,7 @@ void roundTripWithPrependingNewlines() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -294,8 +316,7 @@ void roundTripWithKeepsCRLFLineBreakStyle() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string // need to reconfigure writer to use "\r\n" @@ -318,8 +339,7 @@ void roundTripWithKeepsLFLineBreakStyle() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string // need to reconfigure writer to use "\n" @@ -344,8 +364,7 @@ void roundTripWithModification() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // Modify entry entry.setField(StandardField.AUTHOR, "BlaBla"); @@ -382,8 +401,7 @@ void roundTripWithCamelCasingInTheOriginalEntryAndResultInLowerCase() throws IOE // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // modify entry entry.setField(StandardField.AUTHOR, "BlaBla"); @@ -422,8 +440,7 @@ void entryTypeChange() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(expected)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // modify entry entry.setType(StandardEntryType.InProceedings); @@ -458,8 +475,7 @@ void roundTripWithAppendedNewlines() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -483,8 +499,7 @@ void roundTripNormalizesNewLines() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -522,8 +537,7 @@ void multipleWritesWithoutModification() throws IOException { private String testSingleWrite(String bibtexEntry) throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string StringWriter writer = new StringWriter(); @@ -549,8 +563,7 @@ void monthFieldSpecialSyntax() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // check month field Set fields = entry.getFields(); @@ -585,8 +598,7 @@ void customTypeCanBewritten() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); entry.setField(FieldFactory.parseField("location"), "NY"); @@ -663,8 +675,7 @@ void filenameIsUnmodifiedDuringWrite() throws Exception { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -685,8 +696,7 @@ void addFieldWithLongerLength() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // modify entry entry.setField(StandardField.HOWPUBLISHED, "asdf"); @@ -745,8 +755,7 @@ void roundTripWithPrecedingCommentTest() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); @@ -770,8 +779,7 @@ void roundTripWithPrecedingCommentAndModificationTest() throws IOException { // read in bibtex string ParserResult result = new BibtexParser(importFormatPreferences).parse(new StringReader(bibtexEntry)); - Collection entries = result.getDatabase().getEntries(); - BibEntry entry = entries.iterator().next(); + BibEntry entry = result.getDatabase().getEntries().getFirst(); // change the entry entry.setField(StandardField.AUTHOR, "John Doe"); @@ -779,7 +787,6 @@ void roundTripWithPrecedingCommentAndModificationTest() throws IOException { // write out bibtex string bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX); - // @formatter:off String expected = """ % Some random comment that should stay here @Article{test, @@ -789,7 +796,6 @@ void roundTripWithPrecedingCommentAndModificationTest() throws IOException { number = {1}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected, stringWriter.toString()); } @@ -811,7 +817,6 @@ void alphabeticSerialization() throws IOException { bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBLATEX); - // @formatter:off String expected = """ @Article{, author = {Foo Bar}, @@ -824,7 +829,6 @@ void alphabeticSerialization() throws IOException { year = {2019}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected, stringWriter.toString()); } @@ -861,7 +865,6 @@ void serializeAll() throws IOException { String output = bibEntryWriter.serializeAll(List.of(entry1, entry2), BibDatabaseMode.BIBLATEX); - // @formatter:off String expected1 = """ @Article{, author = {Journal Author}, @@ -874,9 +877,7 @@ void serializeAll() throws IOException { year = {2019}, } """.replace("\n", OS.NEWLINE); - // @formatter:on - // @formatter:off String expected2 = """ @Book{, author = {John Book}, @@ -889,12 +890,11 @@ void serializeAll() throws IOException { year = {2020}, } """.replace("\n", OS.NEWLINE); - // @formatter:on assertEquals(expected1 + OS.NEWLINE + expected2, output); } - static Stream testGetFormattedFieldNameData() { + static Stream getFormattedFieldName() { return Stream.of( Arguments.of(" = ", "", 0), Arguments.of("a = ", "a", 0), @@ -906,13 +906,13 @@ static Stream testGetFormattedFieldNameData() { } @ParameterizedTest - @MethodSource("testGetFormattedFieldNameData") + @MethodSource void getFormattedFieldName(String expected, String fieldName, int indent) { Field field = FieldFactory.parseField(fieldName); assertEquals(expected, BibEntryWriter.getFormattedFieldName(field, indent)); } - static Stream testGetLengthOfLongestFieldNameData() { + static Stream getLengthOfLongestFieldName() { return Stream.of( Arguments.of(1, new BibEntry().withField(FieldFactory.parseField("t"), "t")), Arguments.of(5, new BibEntry(EntryTypeFactory.parse("reference")) @@ -922,7 +922,7 @@ static Stream testGetLengthOfLongestFieldNameData() { } @ParameterizedTest - @MethodSource("testGetLengthOfLongestFieldNameData") + @MethodSource void getLengthOfLongestFieldName(int expected, BibEntry entry) { assertEquals(expected, BibEntryWriter.getLengthOfLongestFieldName(entry)); } diff --git a/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java b/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java index d86fddb1b0f..6001ce8a244 100644 --- a/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java +++ b/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java @@ -22,7 +22,7 @@ class FieldWriterTest { private FieldWriter writer; - public static Stream getMarkdowns() { + static Stream keepHashSignInComment() { return Stream.of(Arguments.of(""" # Changelog @@ -42,8 +42,7 @@ public static Stream getMarkdowns() { #### Achievement\s Lorem ipsum dolor sit amet, consectetur adipiscing elit, #### Method - Lorem ipsum dolor sit amet, consectetur adipiscing elit, - """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit,""" ), // source: https://github.com/JabRef/jabref/issues/8303 --> bug2.txt Arguments.of("Particularly, we equip SOVA – a Semantic and Ontological Variability Analysis method") @@ -56,6 +55,14 @@ void setUp() { writer = new FieldWriter(fieldPreferences); } + @ParameterizedTest + @MethodSource + void keepHashSignInComment(String text) throws Exception { + String writeResult = writer.write(StandardField.COMMENT, text); + String resultWithLfAsNewLineSeparator = StringUtil.unifyLineBreaks(writeResult, "\n"); + assertEquals("{" + text + "}", resultWithLfAsNewLineSeparator); + } + @Test void noNormalizationOfNewlinesInAbstractField() throws Exception { String text = "lorem" + OS.NEWLINE + " ipsum lorem ipsum\nlorem ipsum \rlorem ipsum\r\ntest"; @@ -96,9 +103,14 @@ void preserveNewlineInReviewField() throws Exception { } @Test - void removeWhitespaceFromNonMultiLineFields() throws Exception { + void whitespaceFromNonMultiLineFieldsKept() throws Exception { + // This was a decision on 2024-06-15 when fixing https://github.com/JabRef/jabref/issues/4877 + // We want to have a clean architecture for reading and writing + // Normalizing is done during write (and not during read) + // Furthermore, normalizing is done in the BibDatabaseWriter#applySaveActions and not in the fielld writer + String original = "I\nshould\nnot\ninclude\nadditional\nwhitespaces \nor\n\ttabs."; - String expected = "{I should not include additional whitespaces or tabs.}"; + String expected = "{" + original + "}"; String title = writer.write(StandardField.TITLE, original); String any = writer.write(new UnknownField("anyotherfield"), original); @@ -141,11 +153,53 @@ void hashEnclosedWordsGetRealStringsInMonthField() throws Exception { assertEquals("jan # { - } # feb", writer.write(StandardField.MONTH, text)); } - @ParameterizedTest - @MethodSource("getMarkdowns") - void keepHashSignInComment(String text) throws Exception { - String writeResult = writer.write(StandardField.COMMENT, text); - String resultWithLfAsNewLineSeparator = StringUtil.unifyLineBreaks(writeResult, "\n"); - assertEquals("{" + text + "}", resultWithLfAsNewLineSeparator); + @Test + void hashWorksSimple() throws Exception { + String text = "#text"; + assertEquals("{#text}", writer.write(StandardField.MONTH, text)); + } + + @Test + void escapedHashWorksSimple() throws Exception { + String text = "\\#text"; + assertEquals("{\\#text}", writer.write(StandardField.MONTH, text)); + } + + @Test + void doubleHashesRemoved() throws Exception { + String text = "te##xt"; + assertEquals("{text}", writer.write(StandardField.MONTH, text)); + } + + @Test + void multipleSpacesNotShrunkOnSingleLineField() throws Exception { + String text = "t w o"; + assertEquals("{t w o}", writer.write(StandardField.MONTH, text)); + } + + @Test + void doubleSpacesAreKept() throws Exception { + String text = " text "; + assertEquals("{ text }", writer.write(StandardField.MONTH, text)); + } + + @Test + void spacesAreNotTrimmedAtMultilineField() throws Exception { + String text = " text "; + assertEquals("{ text }", writer.write(StandardField.COMMENT, text)); + // Note: Spaces are trimmed at BibDatabaseWriter#applySaveActions + } + + @Test + void multipleSpacesKeptOnMultiLineField() throws Exception { + String text = "t w o"; + assertEquals("{t w o}", writer.write(StandardField.COMMENT, text)); + } + + @Test + void finalNewLineIsKeptAtMultilineField() throws Exception { + String text = " text " + OS.NEWLINE; + assertEquals("{" + text + "}", writer.write(StandardField.COMMENT, text)); + // Note: Spaces are trimmed at BibDatabaseWriter#applySaveActions } } diff --git a/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java b/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java index ab9a6da112c..dfc1e5ed9d3 100644 --- a/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java +++ b/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java @@ -23,8 +23,7 @@ public class DuplicateCheckTest { private BibEntry simpleArticle; private BibEntry unrelatedArticle; - private BibEntry simpleInbook; - private BibEntry simpleIncollection; + private BibEntry simpleInBook; private DuplicateCheck duplicateChecker; private static BibEntry getSimpleArticle() { @@ -34,7 +33,7 @@ private static BibEntry getSimpleArticle() { .withField(StandardField.YEAR, "2017"); } - private static BibEntry getSimpleIncollection() { + private static BibEntry getSimpleInCollection() { return new BibEntry(StandardEntryType.InCollection) .withField(StandardField.TITLE, "Innovation and Intellectual Property Rights") .withField(StandardField.AUTHOR, "Ove Grandstrand") @@ -43,7 +42,7 @@ private static BibEntry getSimpleIncollection() { .withField(StandardField.YEAR, "2004"); } - private static BibEntry getSimpleInbook() { + private static BibEntry getSimpleInBook() { return new BibEntry(StandardEntryType.InBook) .withField(StandardField.TITLE, "Alice in Wonderland") .withField(StandardField.AUTHOR, "Charles Lutwidge Dodgson") @@ -64,8 +63,7 @@ private static BibEntry getUnrelatedArticle() { public void setUp() { simpleArticle = getSimpleArticle(); unrelatedArticle = getUnrelatedArticle(); - simpleInbook = getSimpleInbook(); - simpleIncollection = getSimpleIncollection(); + simpleInBook = getSimpleInBook(); duplicateChecker = new DuplicateCheck(new BibEntryTypesManager()); } @@ -393,15 +391,15 @@ public void twoEntriesWithSameISBNButDifferentTypesAreNotDuplicates() { public static Stream twoEntriesWithDifferentSpecificFieldsAreNotDuplicates() { return Stream.of( // twoInbooksWithDifferentChaptersAreNotDuplicates - Arguments.of(getSimpleInbook(), StandardField.CHAPTER, + Arguments.of(getSimpleInBook(), StandardField.CHAPTER, "Chapter One – Down the Rabbit Hole", "Chapter Two – The Pool of Tears"), // twoInbooksWithDifferentPagesAreNotDuplicates - Arguments.of(getSimpleInbook(), StandardField.PAGES, "1-20", "21-40"), + Arguments.of(getSimpleInBook(), StandardField.PAGES, "1-20", "21-40"), // twoIncollectionsWithDifferentChaptersAreNotDuplicates - Arguments.of(getSimpleIncollection(), StandardField.CHAPTER, "10", "9"), + Arguments.of(getSimpleInCollection(), StandardField.CHAPTER, "10", "9"), // twoEntriesWithDifferentSpecificFieldsAreNotDuplicates - Arguments.of(getSimpleIncollection(), StandardField.PAGES, "1-20", "21-40") + Arguments.of(getSimpleInCollection(), StandardField.PAGES, "1-20", "21-40") ); } @@ -418,9 +416,9 @@ private void twoEntriesWithDifferentSpecificFieldsAreNotDuplicates(final BibEntr @Test public void inbookWithoutChapterCouldBeDuplicateOfInbookWithChapter() { - final BibEntry inbook2 = ((BibEntry) simpleInbook.clone()).withField(StandardField.CHAPTER, ""); + final BibEntry inbook2 = ((BibEntry) simpleInBook.clone()).withField(StandardField.CHAPTER, ""); - assertTrue(duplicateChecker.isDuplicate(simpleInbook, inbook2, BibDatabaseMode.BIBTEX)); + assertTrue(duplicateChecker.isDuplicate(simpleInBook, inbook2, BibDatabaseMode.BIBTEX)); } @Test diff --git a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java index 0edaae41445..b4746826ccb 100644 --- a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java +++ b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java @@ -175,15 +175,14 @@ void writeEntryWithDuplicateKeywords() throws Exception { databaseWriter.savePartOfDatabase(bibtexContext, Collections.singletonList(entry)); assertEquals("@Article{," + OS.NEWLINE - + " keywords = {asdf,asdf,asdf}," + OS.NEWLINE - + "}" + OS.NEWLINE, + + " keywords = {asdf,asdf,asdf}," + OS.NEWLINE + + "}" + OS.NEWLINE, stringWriter.toString()); } @Test void putKeyWordsRemovesDuplicateKeywordsIsVisibleDuringWrite() throws Exception { - BibEntry entry = new BibEntry(); - entry.setType(StandardEntryType.Article); + BibEntry entry = new BibEntry(StandardEntryType.Article); entry.putKeywords(List.of("asdf", "asdf", "asdf"), ','); database.insertEntry(entry); @@ -191,8 +190,8 @@ void putKeyWordsRemovesDuplicateKeywordsIsVisibleDuringWrite() throws Exception databaseWriter.savePartOfDatabase(bibtexContext, Collections.singletonList(entry)); assertEquals("@Article{," + OS.NEWLINE - + " keywords = {asdf}," + OS.NEWLINE - + "}" + OS.NEWLINE, + + " keywords = {asdf}," + OS.NEWLINE + + "}" + OS.NEWLINE, stringWriter.toString()); } @@ -427,6 +426,10 @@ void roundtripWithArticleMonths() throws Exception { BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData()); + // .gitattributes sets the .bib files to have LF line endings + // This needs to be reflected here + bibWriter = new BibWriter(stringWriter, "\n"); + initializeDatabaseWriter(); databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); assertEquals(Files.readString(testBibtexFile, encoding), stringWriter.toString()); } @@ -454,7 +457,7 @@ void roundtripUtf8EncodingHeaderRemoved() throws Exception { " number = {1}," + OS.NEWLINE + "}" + OS.NEWLINE; // @formatter:on - assertEquals(expected, stringWriter.toString()); + assertEquals(expected, stringWriter.toString()); } @Test @@ -556,6 +559,10 @@ void roundtripWithUserComment() throws Exception { BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData()); + // .gitattributes sets the .bib files to have LF line endings + // This needs to be reflected here + bibWriter = new BibWriter(stringWriter, "\n"); + initializeDatabaseWriter(); databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); assertEquals(Files.readString(testBibtexFile, encoding), stringWriter.toString()); } @@ -635,6 +642,10 @@ void roundtripWithUserCommentAndEntryChange() throws Exception { BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData()); + // .gitattributes sets the .bib files to have LF line endings + // This needs to be reflected here + bibWriter = new BibWriter(stringWriter, "\n"); + initializeDatabaseWriter(); databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); assertEquals(Files.readString(Path.of("src/test/resources/testbib/bibWithUserCommentAndEntryChange.bib"), encoding), stringWriter.toString()); } @@ -652,6 +663,10 @@ void roundtripWithUserCommentBeforeStringAndChange() throws Exception { BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData()); + // .gitattributes sets the .bib files to have LF line endings + // This needs to be reflected here + bibWriter = new BibWriter(stringWriter, "\n"); + initializeDatabaseWriter(); databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); assertEquals(Files.readString(testBibtexFile, encoding), stringWriter.toString()); @@ -665,15 +680,18 @@ void roundtripWithUnknownMetaData() throws Exception { BibDatabaseContext context = new BibDatabaseContext(result.getDatabase(), result.getMetaData()); + // .gitattributes sets the .bib files to have LF line endings + // This needs to be reflected here + bibWriter = new BibWriter(stringWriter, "\n"); + initializeDatabaseWriter(); databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries()); assertEquals(Files.readString(testBibtexFile, encoding), stringWriter.toString()); } @Test void writeSavedSerializationOfEntryIfUnchanged() throws Exception { - BibEntry entry = new BibEntry(); - entry.setType(StandardEntryType.Article); - entry.setField(StandardField.AUTHOR, "Mr. author"); + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Mr. author"); entry.setParsedSerialization("presaved serialization"); entry.setChanged(false); database.insertEntry(entry); @@ -735,12 +753,12 @@ void writeSaveActions() throws Exception { // The order should be kept (the cleanups are a list, not a set) assertEquals("@Comment{jabref-meta: saveActions:enabled;" - + OS.NEWLINE - + "title[lower_case]" + OS.NEWLINE - + "journal[title_case]" + OS.NEWLINE - + "day[upper_case]" + OS.NEWLINE - + ";}" - + OS.NEWLINE, stringWriter.toString()); + + OS.NEWLINE + + "title[lower_case]" + OS.NEWLINE + + "journal[title_case]" + OS.NEWLINE + + "day[upper_case]" + OS.NEWLINE + + ";}" + + OS.NEWLINE, stringWriter.toString()); } @Test @@ -887,10 +905,30 @@ void writeEntriesInOriginalOrderWhenNoSaveOrderConfigIsSetInMetadata() throws Ex stringWriter.toString()); } + @Test + void normalizeWhitespacesCleanupOnlyInTextFields() throws Exception { + BibEntry firstEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Firstname1 Lastname1 and Firstname2 Lastname2") + .withField(StandardField.FILE, "some -- filename -- spaces.pdf") + .withChanged(true); + + database.insertEntry(firstEntry); + + databaseWriter.savePartOfDatabase(bibtexContext, database.getEntries()); + + assertEquals(""" + @Article{, + author = {Firstname1 Lastname1 and Firstname2 Lastname2}, + file = {some -- filename -- spaces.pdf}, + } + """.replace("\n", OS.NEWLINE), stringWriter.toString()); + } + @Test void trimFieldContents() throws IOException { - BibEntry entry = new BibEntry(StandardEntryType.Article); - entry.setField(StandardField.NOTE, " some note \t"); + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.NOTE, " some note \t") + .withChanged(true); database.insertEntry(entry); databaseWriter.saveDatabase(bibtexContext); @@ -977,8 +1015,7 @@ void saveAlsoSavesSecondModification() throws Exception { " journal = {International Journal of Something}," + OS.NEWLINE + " note = {some note}," + OS.NEWLINE + " number = {1}," + OS.NEWLINE + - "}" + OS.NEWLINE + - "" + OS.NEWLINE + + "}" + OS.NEWLINE + OS.NEWLINE + "@Comment{jabref-meta: databaseType:bibtex;}" + OS.NEWLINE, stringWriter.toString()); } diff --git a/src/test/java/org/jabref/logic/bibtex/FieldContentFormatterTest.java b/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatterTest.java similarity index 86% rename from src/test/java/org/jabref/logic/bibtex/FieldContentFormatterTest.java rename to src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatterTest.java index a6705425be5..346cf6c6295 100644 --- a/src/test/java/org/jabref/logic/bibtex/FieldContentFormatterTest.java +++ b/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatterTest.java @@ -1,7 +1,8 @@ -package org.jabref.logic.bibtex; +package org.jabref.logic.formatter.bibtexfields; import java.util.Collections; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; @@ -10,13 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class FieldContentFormatterTest { +class NormalizeWhitespaceFormatterTest { - private FieldContentFormatter parser; + private NormalizeWhitespaceFormatter parser; @BeforeEach void setUp() { - parser = new FieldContentFormatter(new FieldPreferences( + parser = new NormalizeWhitespaceFormatter(new FieldPreferences( false, Collections.emptyList(), Collections.emptyList())); diff --git a/src/test/java/org/jabref/logic/importer/fetcher/ArXivFetcherTest.java b/src/test/java/org/jabref/logic/importer/fetcher/ArXivFetcherTest.java index 6f68a5d25d2..09e2a8036a6 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/ArXivFetcherTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/ArXivFetcherTest.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.net.URL; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.StringJoiner; @@ -11,6 +10,7 @@ import javafx.collections.FXCollections; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.ImportFormatPreferences; @@ -107,29 +107,6 @@ void eachSetUp() { .withField(StandardField.PUBLISHER, "arXiv") .withField(StandardField.DOI, "10.48550/ARXIV.1811.10364"); - // Example of a robust result, with information from both ArXiv-assigned and user-assigned DOIs - // FixMe: Test this BibEntry - BibEntry completePaper = new BibEntry(StandardEntryType.Article) - .withField(StandardField.AUTHOR, "Büscher, Tobias and Diez, Angel L. and Gompper, Gerhard and Elgeti, Jens") - .withField(StandardField.TITLE, "Instability and fingering of interfaces in growing tissue") - .withField(StandardField.DATE, "2020-03-10") - .withField(StandardField.YEAR, "2020") - .withField(StandardField.MONTH, "aug") - .withField(StandardField.NUMBER, "8") - .withField(StandardField.VOLUME, "22") - .withField(StandardField.PAGES, "083005") - .withField(StandardField.PUBLISHER, "{IOP} Publishing") - .withField(StandardField.JOURNAL, "New Journal of Physics") - .withField(StandardField.ABSTRACT, "Interfaces in tissues are ubiquitous, both between tissue and environment as well as between populations of different cell types. The propagation of an interface can be driven mechanically. % e.g. by a difference in the respective homeostatic stress of the different cell types. Computer simulations of growing tissues are employed to study the stability of the interface between two tissues on a substrate. From a mechanical perspective, the dynamics and stability of this system is controlled mainly by four parameters of the respective tissues: (i) the homeostatic stress (ii) cell motility (iii) tissue viscosity and (iv) substrate friction. For propagation driven by a difference in homeostatic stress, the interface is stable for tissue-specific substrate friction even for very large differences of homeostatic stress; however, it becomes unstable above a critical stress difference when the tissue with the larger homeostatic stress has a higher viscosity. A small difference in directed bulk motility between the two tissues suffices to result in propagation with a stable interface, even for otherwise identical tissues. Larger differences in motility force, however, result in a finite-wavelength instability of the interface. Interestingly, the instability is apparently bound by nonlinear effects and the amplitude of the interface undulations only grows to a finite value in time.") - .withField(StandardField.DOI, "10.1088/1367-2630/ab9e88") - .withField(StandardField.EPRINT, "2003.04601") - .withField(StandardField.FILE, ":http\\://arxiv.org/pdf/2003.04601v1:PDF") - .withField(StandardField.EPRINTTYPE, "arXiv") - .withField(StandardField.EPRINTCLASS, "q-bio.TO") - .withField(StandardField.KEYWORDS, "Tissues and Organs (q-bio.TO), FOS: Biological sciences") - .withField(InternalField.KEY_FIELD, "B_scher_2020") - .withField(new UnknownField("copyright"), "arXiv.org perpetual, non-exclusive license"); - sliceTheoremPaper = new BibEntry(StandardEntryType.Article) .withField(StandardField.AUTHOR, "Diez, Tobias") .withField(StandardField.TITLE, "Slice theorem for Fréchet group actions and covariant symplectic field theory") @@ -180,7 +157,10 @@ public void supportsAuthorSearch() throws FetcherException { getInputTestAuthors().forEach(queryBuilder::add); List result = getFetcher().performSearch(queryBuilder.toString()); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); assertFalse(result.isEmpty()); result.forEach(bibEntry -> { @@ -197,7 +177,9 @@ public void noSupportsAuthorSearchWithLastFirstName() throws FetcherException { getTestAuthors().forEach(queryBuilder::add); List result = getFetcher().performSearch(queryBuilder.toString()); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); assertEquals(List.of(), result); } @@ -318,13 +300,13 @@ void findFullTextTrustLevel() { @Test void searchEntryByPartOfTitle() throws Exception { - assertEquals(Collections.singletonList(mainResultPaper), + assertEquals(List.of(mainResultPaper), fetcher.performSearch("title:\"the architecture of mr. dLib's\"")); } @Test void searchEntryByPartOfTitleWithAcuteAccent() throws Exception { - assertEquals(Collections.singletonList(sliceTheoremPaper), + assertEquals(List.of(sliceTheoremPaper), fetcher.performSearch("title:\"slice theorem for Fréchet\"")); } @@ -463,11 +445,12 @@ public void supportsPhraseSearchAndMatchesExact() throws Exception { List resultWithPhraseSearch = fetcher.performSearch("title:\"Taxonomy of Distributed\""); // There is only a single paper found by searching that contains the exact sequence "Taxonomy of Distributed" in the title. - assertEquals(Collections.singletonList(expected), resultWithPhraseSearch); + assertEquals(List.of(expected), resultWithPhraseSearch); } @Test public void supportsBooleanANDSearch() throws Exception { + // Example of a robust result, with information from both ArXiv-assigned and user-assigned DOIs BibEntry expected = new BibEntry(StandardEntryType.Article) .withField(StandardField.AUTHOR, "Büscher, Tobias and Diez, Angel L. and Gompper, Gerhard and Elgeti, Jens") .withField(StandardField.TITLE, "Instability and fingering of interfaces in growing tissue") @@ -479,7 +462,7 @@ public void supportsBooleanANDSearch() throws Exception { .withField(StandardField.ISSN, "1367-2630") .withField(StandardField.PAGES, "083005") .withField(StandardField.PUBLISHER, "IOP Publishing") - .withField(StandardField.JOURNAL, "New Journal of Physics") + .withField(StandardField.JOURNAL, "New J. Phys., 22, 083005 (2020)") .withField(StandardField.ABSTRACT, "Interfaces in tissues are ubiquitous, both between tissue and environment as well as between populations of different cell types. The propagation of an interface can be driven mechanically. % e.g. by a difference in the respective homeostatic stress of the different cell types. Computer simulations of growing tissues are employed to study the stability of the interface between two tissues on a substrate. From a mechanical perspective, the dynamics and stability of this system is controlled mainly by four parameters of the respective tissues: (i) the homeostatic stress (ii) cell motility (iii) tissue viscosity and (iv) substrate friction. For propagation driven by a difference in homeostatic stress, the interface is stable for tissue-specific substrate friction even for very large differences of homeostatic stress; however, it becomes unstable above a critical stress difference when the tissue with the larger homeostatic stress has a higher viscosity. A small difference in directed bulk motility between the two tissues suffices to result in propagation with a stable interface, even for otherwise identical tissues. Larger differences in motility force, however, result in a finite-wavelength instability of the interface. Interestingly, the instability is apparently bound by nonlinear effects and the amplitude of the interface undulations only grows to a finite value in time.") .withField(StandardField.DOI, "10.1088/1367-2630/ab9e88") .withField(StandardField.EPRINT, "2003.04601") @@ -493,7 +476,7 @@ public void supportsBooleanANDSearch() throws Exception { List result = fetcher.performSearch("author:\"Tobias Büscher\" AND title:\"Instability and fingering of interfaces\""); // There is only one paper authored by Tobias Büscher with that phrase in the title - assertEquals(Collections.singletonList(expected), result); + assertEquals(List.of(expected), result); } @Test diff --git a/src/test/java/org/jabref/logic/importer/fetcher/CompositeSearchBasedFetcherTest.java b/src/test/java/org/jabref/logic/importer/fetcher/CompositeSearchBasedFetcherTest.java index 6ea436a1484..9f42028c367 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/CompositeSearchBasedFetcherTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/CompositeSearchBasedFetcherTest.java @@ -9,6 +9,7 @@ import javafx.collections.FXCollections; +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.ImportFormatPreferences; @@ -70,7 +71,9 @@ public void performSearchOnEmptyQuery(Set fetchers) throws E @MethodSource("performSearchParameters") public void performSearchOnNonEmptyQuery(Set fetchers) throws Exception { CompositeSearchBasedFetcher compositeFetcher = new CompositeSearchBasedFetcher(fetchers, importerPreferences, Integer.MAX_VALUE); - ImportCleanup cleanup = ImportCleanup.targeting(BibDatabaseMode.BIBTEX); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup cleanup = ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences); List compositeResult = compositeFetcher.performSearch("quantum"); for (SearchBasedFetcher fetcher : fetchers) { diff --git a/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java b/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java index dbcefcbb899..71221813b12 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java @@ -31,9 +31,13 @@ public void setUp() { entry.setType(StandardEntryType.Article); entry.setCitationKey("DBLP:journals/stt/GeigerHL16"); - entry.setField(StandardField.TITLE, - "Process Engine Benchmarking with Betsy in the Context of {ISO/IEC} Quality Standards"); - entry.setField(StandardField.AUTHOR, "Matthias Geiger and Simon Harrer and J{\\\"{o}}rg Lenhard"); + entry.setField(StandardField.TITLE, """ + Process Engine Benchmarking with Betsy in the Context of {ISO/IEC} + Quality Standards"""); + entry.setField(StandardField.AUTHOR, """ + Matthias Geiger and + Simon Harrer and + J{\\\"{o}}rg Lenhard"""); entry.setField(StandardField.JOURNAL, "Softwaretechnik-Trends"); entry.setField(StandardField.VOLUME, "36"); entry.setField(StandardField.NUMBER, "2"); diff --git a/src/test/java/org/jabref/logic/importer/fetcher/MathSciNetTest.java b/src/test/java/org/jabref/logic/importer/fetcher/MathSciNetTest.java index c218ac61a54..264986c3c50 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/MathSciNetTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/MathSciNetTest.java @@ -2,8 +2,11 @@ import java.io.InputStream; import java.util.List; -import java.util.Optional; +import javafx.collections.FXCollections; + +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.cleanup.NormalizeWhitespacesCleanup; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; @@ -22,58 +25,69 @@ @FetcherTest class MathSciNetTest { - MathSciNet fetcher; + private MathSciNet fetcher; private BibEntry ratiuEntry; + private NormalizeWhitespacesCleanup normalizeWhitespacesCleanup; @BeforeEach void setUp() throws Exception { ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); when(importFormatPreferences.bibEntryPreferences().getKeywordSeparator()).thenReturn(','); fetcher = new MathSciNet(importFormatPreferences); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + normalizeWhitespacesCleanup = new NormalizeWhitespacesCleanup(fieldPreferences); - ratiuEntry = new BibEntry(); - ratiuEntry.setType(StandardEntryType.Article); - ratiuEntry.setCitationKey("MR3537908"); - ratiuEntry.setField(StandardField.AUTHOR, "Chechkin, Gregory A. and Ratiu, Tudor S. and Romanov, Maxim S. and Samokhin, Vyacheslav N."); - ratiuEntry.setField(StandardField.TITLE, "Existence and uniqueness theorems for the two-dimensional {E}ricksen-{L}eslie system"); - ratiuEntry.setField(StandardField.JOURNAL, "Journal of Mathematical Fluid Mechanics"); - ratiuEntry.setField(StandardField.VOLUME, "18"); - ratiuEntry.setField(StandardField.YEAR, "2016"); - ratiuEntry.setField(StandardField.NUMBER, "3"); - ratiuEntry.setField(StandardField.PAGES, "571--589"); - ratiuEntry.setField(StandardField.KEYWORDS, "76A15 (35A01 35A02 35K61 82D30)"); - ratiuEntry.setField(StandardField.MR_NUMBER, "3537908"); - ratiuEntry.setField(StandardField.ISSN, "1422-6928,1422-6952"); - ratiuEntry.setField(StandardField.DOI, "10.1007/s00021-016-0250-0"); + ratiuEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("MR3537908") + .withField(StandardField.AUTHOR, "Chechkin, Gregory A. and Ratiu, Tudor S. and Romanov, Maxim S. and Samokhin, Vyacheslav N.") + .withField(StandardField.TITLE, "Existence and uniqueness theorems for the two-dimensional {E}ricksen-{L}eslie system") + .withField(StandardField.JOURNAL, "Journal of Mathematical Fluid Mechanics") + .withField(StandardField.VOLUME, "18") + .withField(StandardField.YEAR, "2016") + .withField(StandardField.NUMBER, "3") + .withField(StandardField.PAGES, "571--589") + .withField(StandardField.KEYWORDS, "76A15 (35A01 35A02 35K61 82D30)") + .withField(StandardField.MR_NUMBER, "3537908") + .withField(StandardField.ISSN, "1422-6928,1422-6952") + .withField(StandardField.DOI, "10.1007/s00021-016-0250-0"); } @Test void searchByEntryFindsEntry() throws Exception { - BibEntry searchEntry = new BibEntry(); - searchEntry.setField(StandardField.TITLE, "existence"); - searchEntry.setField(StandardField.AUTHOR, "Ratiu"); - searchEntry.setField(StandardField.JOURNAL, "fluid"); + BibEntry searchEntry = new BibEntry() + .withField(StandardField.TITLE, "existence") + .withField(StandardField.AUTHOR, "Ratiu") + .withField(StandardField.JOURNAL, "fluid"); List fetchedEntries = fetcher.performSearch(searchEntry); + if (!fetchedEntries.isEmpty()) { + normalizeWhitespacesCleanup.cleanup(fetchedEntries.getFirst()); + } assertEquals(List.of(ratiuEntry), fetchedEntries); } @Test - @DisabledOnCIServer("CI server has no subscription to MathSciNet and thus gets 401 response") + @DisabledOnCIServer("CI server has no subscription to MathSciNet and thus gets 401 response. One single call goes through, but subsequent calls fail.") void searchByIdInEntryFindsEntry() throws Exception { - BibEntry searchEntry = new BibEntry(); - searchEntry.setField(StandardField.MR_NUMBER, "3537908"); + BibEntry searchEntry = new BibEntry() + .withField(StandardField.MR_NUMBER, "3537908"); List fetchedEntries = fetcher.performSearch(searchEntry); + if (!fetchedEntries.isEmpty()) { + normalizeWhitespacesCleanup.cleanup(fetchedEntries.getFirst()); + } assertEquals(List.of(ratiuEntry), fetchedEntries); } @Test - @DisabledOnCIServer("CI server has no subscription to MathSciNet and thus gets 401 response") + @DisabledOnCIServer("CI server has no subscription to MathSciNet and thus gets 401 response. One single call goes through, but subsequent calls fail.") void searchByQueryFindsEntry() throws Exception { List fetchedEntries = fetcher.performSearch("Existence and uniqueness theorems Two-Dimensional Ericksen Leslie System"); assertFalse(fetchedEntries.isEmpty()); - assertEquals(ratiuEntry, fetchedEntries.get(1)); + BibEntry secondEntry = fetchedEntries.get(1); + normalizeWhitespacesCleanup.cleanup(secondEntry); + assertEquals(ratiuEntry, secondEntry); } @Test @@ -81,8 +95,7 @@ void getParser() throws Exception { String fileName = "mathscinet.json"; try (InputStream is = MathSciNetTest.class.getResourceAsStream(fileName)) { List entries = fetcher.getParser().parseEntries(is); - - assertEquals(Optional.of( + assertEquals( new BibEntry(StandardEntryType.Article) .withField(StandardField.TITLE, "On the weights of general MDS codes") .withField(StandardField.AUTHOR, "Alderson, Tim L.") @@ -93,8 +106,8 @@ void getParser() throws Exception { .withField(StandardField.PAGES, "5414--5418") .withField(StandardField.MR_NUMBER, "4158623") .withField(StandardField.KEYWORDS, "Bounds on codes") - .withField(StandardField.DOI, "10.1109/TIT.2020.2977319") - ), entries.stream().findFirst()); + .withField(StandardField.DOI, "10.1109/TIT.2020.2977319"), + entries.getFirst()); } } } diff --git a/src/test/java/org/jabref/logic/importer/fetcher/SearchBasedFetcherCapabilityTest.java b/src/test/java/org/jabref/logic/importer/fetcher/SearchBasedFetcherCapabilityTest.java index a93f73bed17..8773a2d84b6 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/SearchBasedFetcherCapabilityTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/SearchBasedFetcherCapabilityTest.java @@ -6,6 +6,9 @@ import java.util.StringJoiner; import java.util.stream.Collectors; +import javafx.collections.FXCollections; + +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.SearchBasedFetcher; import org.jabref.model.database.BibDatabaseMode; @@ -18,6 +21,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Defines the set of capability tests that each tests a given search capability, e.g. author based search. @@ -36,7 +41,9 @@ default void supportsAuthorSearch() throws Exception { getTestAuthors().forEach(queryBuilder::add); List result = getFetcher().performSearch(queryBuilder.toString()); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); assertFalse(result.isEmpty()); result.forEach(bibEntry -> { @@ -53,7 +60,9 @@ default void supportsAuthorSearch() throws Exception { @Test default void supportsYearSearch() throws Exception { List result = getFetcher().performSearch("year:" + getTestYear()); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); List differentYearsInResult = result.stream() .map(bibEntry -> bibEntry.getField(StandardField.YEAR)) .filter(Optional::isPresent) @@ -72,7 +81,9 @@ default void supportsYearRangeSearch() throws Exception { List yearsInYearRange = List.of("2018", "2019", "2020"); List result = getFetcher().performSearch("year-range:2018-2020"); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); List differentYearsInResult = result.stream() .map(bibEntry -> bibEntry.getField(StandardField.YEAR)) .filter(Optional::isPresent) @@ -92,7 +103,9 @@ default void supportsYearRangeSearch() throws Exception { @Test default void supportsJournalSearch() throws Exception { List result = getFetcher().performSearch("journal:\"" + getTestJournal() + "\""); - ImportCleanup.targeting(BibDatabaseMode.BIBTEX).doPostCleanup(result); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + ImportCleanup.targeting(BibDatabaseMode.BIBTEX, fieldPreferences).doPostCleanup(result); assertFalse(result.isEmpty()); result.forEach(bibEntry -> { diff --git a/src/test/java/org/jabref/logic/importer/fileformat/BibtexImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/BibtexImporterTest.java index 6aad5b3ab7c..50e82e542c7 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/BibtexImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/BibtexImporterTest.java @@ -63,8 +63,10 @@ public void importEntries() throws IOException, URISyntaxException { if ("aksin".equals(entry.getCitationKey().get())) { assertEquals( Optional.of( - "Aks{\\i}n, {\\\"O}zge and T{\\\"u}rkmen, Hayati and Artok, Levent and {\\c{C}}etinkaya, " - + "Bekir and Ni, Chaoying and B{\\\"u}y{\\\"u}kg{\\\"u}ng{\\\"o}r, Orhan and {\\\"O}zkal, Erhan"), + """ + Aks{\\i}n, {\\"O}zge and T{\\"u}rkmen, Hayati and Artok, Levent + and {\\c{C}}etinkaya, Bekir and Ni, Chaoying and + B{\\"u}y{\\"u}kg{\\"u}ng{\\"o}r, Orhan and {\\"O}zkal, Erhan"""), entry.getField(StandardField.AUTHOR)); assertEquals(Optional.of("aksin"), entry.getCitationKey()); assertEquals(Optional.of("2006"), entry.getField(StandardField.DATE)); @@ -73,23 +75,30 @@ public void importEntries() throws IOException, URISyntaxException { assertEquals(Optional.of("13"), entry.getField(StandardField.NUMBER)); assertEquals(Optional.of("3027-3036"), entry.getField(StandardField.PAGES)); assertEquals(Optional - .of("Effect of immobilization on catalytic characteristics of saturated {Pd-N}-heterocyclic " - + "carbenes in {Mizoroki-Heck} reactions"), + .of(""" + Effect of immobilization on catalytic characteristics of + saturated {Pd-N}-heterocyclic carbenes in {Mizoroki-Heck} + reactions"""), entry.getField(StandardField.TITLE)); assertEquals(Optional.of("691"), entry.getField(StandardField.VOLUME)); } else if ("stdmodel".equals(entry.getCitationKey().get())) { assertEquals(Optional - .of("A \\texttt{set} with three members discussing the standard model of particle physics. " - + "The \\texttt{crossref} field in the \\texttt{@set} entry and the \\texttt{entryset} field in " - + "each set member entry is needed only when using BibTeX as the backend"), + .of(""" + A \\texttt{set} with three members discussing the standard + model of particle physics. The \\texttt{crossref} field + in the \\texttt{@set} entry and the \\texttt{entryset} field in + each set member entry is needed only when using BibTeX as the + backend"""), entry.getField(StandardField.ANNOTATION)); assertEquals(Optional.of("stdmodel"), entry.getCitationKey()); assertEquals(Optional.of("glashow,weinberg,salam"), entry.getField(StandardField.ENTRYSET)); } else if ("set".equals(entry.getCitationKey().get())) { assertEquals(Optional - .of("A \\texttt{set} with three members. The \\texttt{crossref} field in the \\texttt{@set} " - + "entry and the \\texttt{entryset} field in each set member entry is needed only when using " - + "BibTeX as the backend"), + .of(""" + A \\texttt{set} with three members. The \\texttt{crossref} field + in the \\texttt{@set} entry and the \\texttt{entryset} field in + each set member entry is needed only when using BibTeX as the + backend"""), entry.getField(StandardField.ANNOTATION)); assertEquals(Optional.of("set"), entry.getCitationKey()); assertEquals(Optional.of("herrmann,aksin,yoon"), entry.getField(StandardField.ENTRYSET)); diff --git a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java index 8907bea609f..22445e563ce 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java @@ -427,22 +427,17 @@ void parseRecognizesFormatedEntry() throws IOException { url = {http://james.howison.name/publications.html} }))""")); - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); + BibEntry expected = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("CroAnnHow05") + .withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H. and Howison, J. and Masango, C.") + .withField(StandardField.TITLE, "Effective work practices for floss development: A model and propositions") + .withField(StandardField.BOOKTITLE, "Hawaii International Conference On System Sciences (HICSS)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.OWNER, "oezbek") + .withField(StandardField.TIMESTAMP, "2006.05.29") + .withField(StandardField.URL, "http://james.howison.name/publications.html"); - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.InProceedings, entry.getType()); - assertEquals(8, entry.getFields().size()); - assertEquals(Optional.of("CroAnnHow05"), entry.getCitationKey()); - assertEquals(Optional.of("Crowston, K. and Annabi, H. and Howison, J. and Masango, C."), entry.getField(StandardField.AUTHOR)); - assertEquals(Optional.of("Effective work practices for floss development: A model and propositions"), - entry.getField(StandardField.TITLE)); - assertEquals(Optional.of("Hawaii International Conference On System Sciences (HICSS)"), - entry.getField(StandardField.BOOKTITLE)); - assertEquals(Optional.of("2005"), entry.getField(StandardField.YEAR)); - assertEquals(Optional.of("oezbek"), entry.getField(StandardField.OWNER)); - assertEquals(Optional.of("2006.05.29"), entry.getField(StandardField.TIMESTAMP)); - assertEquals(Optional.of("http://james.howison.name/publications.html"), entry.getField(StandardField.URL)); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -522,10 +517,8 @@ void parseReturnsEmptyListIfNoEntryRecognized() throws IOException { timestamp = {2006.05.29}, url = {http://james.howison.name/publications.html} }))""")); - - Collection parsed = result.getDatabase().getEntries(); - - assertEquals(0, parsed.size()); + assertTrue(result.hasWarnings()); + assertEquals(List.of(), result.getDatabase().getEntries()); } @Test @@ -560,15 +553,12 @@ void parseIgnoresAndWarnsAboutEntryWithUnmatchedOpenBracket() throws IOException void parseAddsEscapedOpenBracketToFieldValue() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,review={escaped \\{ bracket}}")); - - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); - - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.Article, entry.getType()); - assertEquals(Optional.of("test"), entry.getCitationKey()); - assertEquals(Optional.of("escaped \\{ bracket"), entry.getField(StandardField.REVIEW)); assertFalse(result.hasWarnings()); + + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.REVIEW, "escaped \\{ bracket"); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -576,14 +566,11 @@ void parseAddsEscapedClosingBracketToFieldValue() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,review={escaped \\} bracket}}")); - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.REVIEW, "escaped \\} bracket"); - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.Article, entry.getType()); - assertEquals(Optional.of("test"), entry.getCitationKey()); - assertEquals(Optional.of("escaped \\} bracket"), entry.getField(StandardField.REVIEW)); - assertFalse(result.hasWarnings()); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -598,7 +585,7 @@ void parseIgnoresAndWarnsAboutEntryWithUnmatchedOpenBracketInQuotationMarks() th } @Test - void parseIgnoresArbitraryContentAfterEntry() throws IOException { + void parseMovesArbitraryContentAfterEntryToEpilog() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,author={author bracket }}}")); @@ -643,14 +630,11 @@ void parseRecognizesEntryWithAtSymbolInQuotationMarks() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,author=\"author @ good\"}")); - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "author @ good"); - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.Article, entry.getType()); - assertEquals(Optional.of("test"), entry.getCitationKey()); - assertEquals(2, entry.getFields().size()); - assertEquals(Optional.of("author @ good"), entry.getField(StandardField.AUTHOR)); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -658,14 +642,11 @@ void parseRecognizesFieldsWithBracketsEnclosedInQuotationMarks() throws IOExcept ParserResult result = parser .parse(new StringReader("@article{test,author=\"Test {Ed {von} Test}\"}")); - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test {Ed {von} Test}"); - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.Article, entry.getType()); - assertEquals(Optional.of("test"), entry.getCitationKey()); - assertEquals(2, entry.getFields().size()); - assertEquals(Optional.of("Test {Ed {von} Test}"), entry.getField(StandardField.AUTHOR)); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -674,14 +655,11 @@ void parseRecognizesFieldsWithEscapedQuotationMarks() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,author=\"Test {\" Test}\"}")); - Collection parsed = result.getDatabase().getEntries(); - BibEntry entry = parsed.iterator().next(); + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test {\" Test}"); - assertEquals(1, parsed.size()); - assertEquals(StandardEntryType.Article, entry.getType()); - assertEquals(Optional.of("test"), entry.getCitationKey()); - assertEquals(2, entry.getFields().size()); - assertEquals(Optional.of("Test {\" Test}"), entry.getField(StandardField.AUTHOR)); + assertEquals(List.of(expected), result.getDatabase().getEntries()); } @Test @@ -689,9 +667,7 @@ void parseIgnoresAndWarnsAboutEntryWithFieldsThatAreNotSeperatedByComma() throws ParserResult result = parser .parse(new StringReader("@article{test,author={Ed von Test} year=2005}")); - Collection parsed = result.getDatabase().getEntries(); - - assertEquals(0, parsed.size()); + assertEquals(List.of(), result.getDatabase().getEntries()); assertTrue(result.hasWarnings()); } @@ -890,30 +866,28 @@ void parseIgnoresComments() throws IOException { ParserResult result = parser .parse(new StringReader("@comment{some text and \\latex}")); - assertEquals(0, result.getDatabase().getEntries().size()); + assertEquals(List.of(), result.getDatabase().getEntries()); } + // TODO: We should keep @comment if it is the only "thing" in the file @Test - void parseIgnoresUpercaseComments() throws IOException { + void parseIgnoresUppercaseComments() throws IOException { ParserResult result = parser .parse(new StringReader("@COMMENT{some text and \\latex}")); - - assertEquals(0, result.getDatabase().getEntries().size()); + assertFalse(result.hasWarnings()); // FIXME: We silently remove @COMMENT + assertEquals(List.of(), result.getDatabase().getEntries()); } @Test - void parseIgnoresCommentsBeforeEntry() throws IOException { + void parseKeepsCommentsAsUserComments() throws IOException { ParserResult result = parser .parse(new StringReader("@comment{some text and \\latex}" + "@article{test,author={Ed von Test}}")); - Collection parsedEntries = result.getDatabase().getEntries(); - BibEntry parsedEntry = parsedEntries.iterator().next(); - - assertEquals(1, parsedEntries.size()); - assertEquals(StandardEntryType.Article, parsedEntry.getType()); - assertEquals(Optional.of("test"), parsedEntry.getCitationKey()); - assertEquals(2, parsedEntry.getFields().size()); - assertEquals(Optional.of("Ed von Test"), parsedEntry.getField(StandardField.AUTHOR)); + assertEquals(List.of(new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Ed von Test") + .withUserComments("@comment{some text and \\latex}")), + result.getDatabase().getEntries()); } @Test @@ -921,14 +895,11 @@ void parseIgnoresCommentsAfterEntry() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,author={Ed von Test}}" + "@comment{some text and \\latex}")); - Collection parsedEntries = result.getDatabase().getEntries(); - BibEntry parsedEntry = parsedEntries.iterator().next(); - - assertEquals(1, parsedEntries.size()); - assertEquals(StandardEntryType.Article, parsedEntry.getType()); - assertEquals(Optional.of("test"), parsedEntry.getCitationKey()); - assertEquals(2, parsedEntry.getFields().size()); - assertEquals(Optional.of("Ed von Test"), parsedEntry.getField(StandardField.AUTHOR)); + assertEquals(List.of(new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Ed von Test")), + result.getDatabase().getEntries()); + assertEquals("@comment{some text and \\latex}", result.getDatabase().getEpilog()); } @Test @@ -970,51 +941,51 @@ void parseIgnoresTextAfterEntry() throws IOException { } @Test - void parseConvertsNewlineToSpace() throws IOException { + void parsKeesNewlines() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,a = {a\nb}}")); Collection parsedEntries = result.getDatabase().getEntries(); BibEntry parsedEntry = parsedEntries.iterator().next(); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("a"))); + assertEquals(Optional.of("a\nb"), parsedEntry.getField(new UnknownField("a"))); } @Test - void parseConvertsMultipleNewlinesToSpace() throws IOException { + void parsKeepsMultipleNewlines() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,a = {a\n\nb}," + "b = {a\n \nb}," + "c = {a \n \n b}}")); Collection parsedEntries = result.getDatabase().getEntries(); BibEntry parsedEntry = parsedEntries.iterator().next(); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("a"))); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("b"))); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("c"))); + assertEquals(Optional.of("a\n\nb"), parsedEntry.getField(new UnknownField("a"))); + assertEquals(Optional.of("a\n \nb"), parsedEntry.getField(new UnknownField("b"))); + assertEquals(Optional.of("a \n \n b"), parsedEntry.getField(new UnknownField("c"))); } @Test - void parseConvertsTabToSpace() throws IOException { + void parseKeepsTabs() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,a = {a\tb}}")); Collection parsedEntries = result.getDatabase().getEntries(); BibEntry parsedEntry = parsedEntries.iterator().next(); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("a"))); + assertEquals(Optional.of("a\tb"), parsedEntry.getField(new UnknownField("a"))); } @Test - void parseConvertsMultipleTabsToSpace() throws IOException { + void parsKeepsMultipleTabs() throws IOException { ParserResult result = parser .parse(new StringReader("@article{test,a = {a\t\tb}," + "b = {a\t \tb}," + "c = {a \t \t b}}")); Collection parsedEntries = result.getDatabase().getEntries(); BibEntry parsedEntry = parsedEntries.iterator().next(); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("a"))); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("b"))); - assertEquals(Optional.of("a b"), parsedEntry.getField(new UnknownField("c"))); + assertEquals(Optional.of("a\t\tb"), parsedEntry.getField(new UnknownField("a"))); + assertEquals(Optional.of("a\t \tb"), parsedEntry.getField(new UnknownField("b"))); + assertEquals(Optional.of("a \t \t b"), parsedEntry.getField(new UnknownField("c"))); } @Test @@ -1921,10 +1892,9 @@ void parseCommentAndEntryInOneLine() throws IOException { @Test void preserveEncodingPrefixInsideEntry() throws ParseException { - BibEntry expected = new BibEntry(); - expected.setType(StandardEntryType.Article); - expected.setCitationKey("test"); - expected.setField(StandardField.AUTHOR, SaveConfiguration.ENCODING_PREFIX); + BibEntry expected = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, SaveConfiguration.ENCODING_PREFIX); List parsed = parser .parseEntries("@article{test,author={" + SaveConfiguration.ENCODING_PREFIX + "}}"); diff --git a/src/test/java/org/jabref/logic/importer/fileformat/MedlinePlainImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/MedlinePlainImporterTest.java index 4e522c3b032..588185f581f 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/MedlinePlainImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/MedlinePlainImporterTest.java @@ -110,13 +110,12 @@ void importMultipleEntriesInSingleFile() throws IOException, URISyntaxException assertEquals(StandardEntryType.InProceedings, testEntry.getType()); assertEquals(Optional.of("Inproceedings book title"), testEntry.getField(StandardField.BOOKTITLE)); - BibEntry expectedEntry5 = new BibEntry(StandardEntryType.Proceedings); - expectedEntry5.setField(StandardField.KEYWORDS, "Female"); + BibEntry expectedEntry5 = new BibEntry(StandardEntryType.Proceedings) + .withField(StandardField.KEYWORDS, "Female"); assertEquals(expectedEntry5, entries.get(5)); - BibEntry expectedEntry6 = new BibEntry(); - expectedEntry6.setType(StandardEntryType.Misc); - expectedEntry6.setField(StandardField.KEYWORDS, "Female"); + BibEntry expectedEntry6 = new BibEntry(StandardEntryType.Misc) + .withField(StandardField.KEYWORDS, "Female"); assertEquals(expectedEntry6, entries.get(6)); } diff --git a/src/test/java/org/jabref/logic/shared/DBMSSynchronizerTest.java b/src/test/java/org/jabref/logic/shared/DBMSSynchronizerTest.java index d075568667f..787c926b131 100644 --- a/src/test/java/org/jabref/logic/shared/DBMSSynchronizerTest.java +++ b/src/test/java/org/jabref/logic/shared/DBMSSynchronizerTest.java @@ -5,6 +5,9 @@ import java.util.List; import java.util.Map; +import javafx.collections.FXCollections; + +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; import org.jabref.logic.cleanup.FieldFormatterCleanup; import org.jabref.logic.cleanup.FieldFormatterCleanups; @@ -30,6 +33,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @DatabaseTest @Execution(ExecutionMode.SAME_THREAD) @@ -61,7 +66,10 @@ public void setup() throws Exception { bibDatabase = new BibDatabase(); BibDatabaseContext context = new BibDatabaseContext(bibDatabase); - dbmsSynchronizer = new DBMSSynchronizer(context, ',', pattern, new DummyFileUpdateMonitor()); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + + dbmsSynchronizer = new DBMSSynchronizer(context, ',', fieldPreferences, pattern, new DummyFileUpdateMonitor()); bibDatabase.registerListener(dbmsSynchronizer); dbmsSynchronizer.openSharedDatabase(dbmsConnection); diff --git a/src/test/java/org/jabref/logic/shared/SynchronizationSimulatorTest.java b/src/test/java/org/jabref/logic/shared/SynchronizationSimulatorTest.java index 89296f267da..7ef73ad9a76 100644 --- a/src/test/java/org/jabref/logic/shared/SynchronizationSimulatorTest.java +++ b/src/test/java/org/jabref/logic/shared/SynchronizationSimulatorTest.java @@ -3,6 +3,9 @@ import java.sql.SQLException; import java.util.List; +import javafx.collections.FXCollections; + +import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -23,6 +26,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @DatabaseTest @Execution(ExecutionMode.SAME_THREAD) @@ -47,13 +52,16 @@ public void setup() throws Exception { DBMSConnection dbmsConnection = ConnectorTest.getTestDBMSConnection(TestManager.getDBMSTypeTestParameter()); TestManager.clearTables(dbmsConnection); + FieldPreferences fieldPreferences = mock(FieldPreferences.class); + when(fieldPreferences.getNonWrappableFields()).thenReturn(FXCollections.observableArrayList()); + clientContextA = new BibDatabaseContext(); - DBMSSynchronizer synchronizerA = new DBMSSynchronizer(clientContextA, ',', pattern, new DummyFileUpdateMonitor()); + DBMSSynchronizer synchronizerA = new DBMSSynchronizer(clientContextA, ',', fieldPreferences, pattern, new DummyFileUpdateMonitor()); clientContextA.convertToSharedDatabase(synchronizerA); clientContextA.getDBMSSynchronizer().openSharedDatabase(dbmsConnection); clientContextB = new BibDatabaseContext(); - DBMSSynchronizer synchronizerB = new DBMSSynchronizer(clientContextB, ',', pattern, new DummyFileUpdateMonitor()); + DBMSSynchronizer synchronizerB = new DBMSSynchronizer(clientContextB, ',', fieldPreferences, pattern, new DummyFileUpdateMonitor()); clientContextB.convertToSharedDatabase(synchronizerB); // use a second connection, because this is another client (typically on another machine) clientContextB.getDBMSSynchronizer().openSharedDatabase(ConnectorTest.getTestDBMSConnection(TestManager.getDBMSTypeTestParameter())); diff --git a/src/test/java/org/jabref/logic/util/io/FileUtilTest.java b/src/test/java/org/jabref/logic/util/io/FileUtilTest.java index 99551587f2a..10c601dc3dd 100644 --- a/src/test/java/org/jabref/logic/util/io/FileUtilTest.java +++ b/src/test/java/org/jabref/logic/util/io/FileUtilTest.java @@ -220,8 +220,8 @@ void uniquePathFragmentWithSameSuffix() { @Test void uniquePathFragmentWithSameSuffixAndLongerName() { - List dirs = List.of("/users/jabref/bibliography.bib", "/users/jabref/koppor-bibliography.bib"); - assertEquals(Optional.of("koppor-bibliography.bib"), FileUtil.getUniquePathFragment(dirs, Path.of("/users/jabref/koppor-bibliography.bib"))); + List paths = List.of("/users/jabref/bibliography.bib", "/users/jabref/koppor-bibliography.bib"); + assertEquals(Optional.of("koppor-bibliography.bib"), FileUtil.getUniquePathFragment(paths, Path.of("/users/jabref/koppor-bibliography.bib"))); } @Test diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/NbibImporterTest.bib b/src/test/resources/org/jabref/logic/importer/fileformat/NbibImporterTest.bib index cad8dd6e27a..f8f7210461f 100644 --- a/src/test/resources/org/jabref/logic/importer/fileformat/NbibImporterTest.bib +++ b/src/test/resources/org/jabref/logic/importer/fileformat/NbibImporterTest.bib @@ -1,5 +1,5 @@ @article{, - abstract = {some things will be described }, + abstract = {some things will be described}, address = {India}, article-doi = {10.4103/2277-9175.180636}, article-pii = {ABR-5-67}, @@ -43,5 +43,4 @@ @article{ volume = {5}, volume-title = {title of the volume}, year = {2016} - } - \ No newline at end of file +}