From 0c26a550c252438b7a0696eaab5c83fa8a148d29 Mon Sep 17 00:00:00 2001 From: Luggas <127773292+Luggas4you@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:58:40 +0100 Subject: [PATCH] Implement test cases for search (#10193) * Move search folder to logic * Add testEmptyLibrarySearch * Add initializeDatabaseFromPath * Add TestLibraryA & testUpperAndLowerWordSearch * Add testSimpleSingleFieldSearch * Add testSimpleMultipleFieldSearch * Add testSensitiveWordSearch * Add testSensitiveMutipleFieldSearch * Add BibEntries for test-library-B * Add testSimpleRegularExpression * Add testSensitiveRegularExpression * SimplePDFFulltextSearch WIP * Change set.Files to add.File * Add testSimplePDFNoteFulltextSearch WIP * Move indexer * Add missing @Tests to SearchFunctionalityTest.java WIP * WIP * Exclude non-functional @Tests * Add withFiles * Remove empty lines * @NullMarked for LinkedFile * Fix Logger parameter * Streamline tests * Fix checkstyle * Refine tests * Get rid of missing "identity" formatter * Minimize test files * Remove obsolete method (and make indexer class variable to speedup) * Fix filenames * Some refactorings * Revert global indexer mapping (does not work) ERROR: Could not retrieve search results.: org.apache.lucene.index.IndexNotFoundException: no segments* file found in NIOFSDirectory@C:\Users\koppor\AppData\Local\Temp\junit11457118934853053051 lockFactory=org.apache.lucene.store.NativeFSLockFactory@5d6751d7: files: [write.lock] * Revert "Revert global indexer mapping (does not work)" This reverts commit e0810441971ad7d456683903ce9bb7d0fbeb729e. * Small code updates * Rename test files * Some more logging * Refine transaction boundaries (and some minor tweaks) * Add TODO * Fix off-by-one error * Remove non-used .bib file * Refine comments * Fix variable assignment * test-library-A -> test-library-title-casing * Merge test cases of library-B.bib into title-casing.bib * Increase transaction boundary for index addition/removal * More readable directory names for index directory * WIP: Introduce PdfIndexerManager * Preparation for "fulltext search not checking all attached files upon start" * Use "right" factory (and rename factor getter) * Fix FullTextSeachRule (refactoring introduced a bug) * Revert property for recheck of attached files * Fix linting issues * Fix Formatters optimization * Fix test * Exception for architecture * Refine .gitignore * Add missing } * Do not search for PDF files in case of an exception of a search * Remove duplicate code (and unneccsary pre-fetch of search results) * Add exception for test * Remove ".getMessage()" * Move comment and remove obsolete variable * Fix typo * Fix name * Add dot * Add comment * Call splitting method * Add JavaDocComment * Revert lambdas --------- Co-authored-by: Oliver Kopp --- CHANGELOG.md | 3 + .../org/jabref/gui/JabRefExecutorService.java | 4 + src/main/java/org/jabref/gui/JabRefFrame.java | 2 + src/main/java/org/jabref/gui/JabRefGUI.java | 2 +- src/main/java/org/jabref/gui/LibraryTab.java | 20 +- .../java/org/jabref/gui/StateManager.java | 7 - .../jabref/gui/entryeditor/CommentsTab.java | 2 +- .../gui/entryeditor/DeprecatedFieldsTab.java | 2 +- .../gui/entryeditor/FieldsEditorTab.java | 2 +- .../gui/entryeditor/OptionalFields2Tab.java | 2 +- .../gui/entryeditor/OptionalFieldsTab.java | 2 +- .../entryeditor/OptionalFieldsTabBase.java | 2 +- .../gui/entryeditor/OtherFieldsTab.java | 2 +- .../jabref/gui/entryeditor/PreviewTab.java | 2 +- .../gui/entryeditor/RequiredFieldsTab.java | 2 +- .../gui/entryeditor/UserDefinedFieldsTab.java | 2 +- .../gui/exporter/SaveDatabaseAction.java | 7 +- .../ExternalFilesEntryLinker.java | 10 +- .../org/jabref/gui/preview/PreviewPanel.java | 2 +- .../RebuildFulltextSearchIndexAction.java | 7 +- .../logic/cleanup/FieldFormatterCleanups.java | 17 +- .../jabref/logic/formatter/Formatters.java | 15 +- .../importer/fileformat/BibtexParser.java | 6 +- .../logic/importer/util/MetaDataParser.java | 4 +- .../search/{indexing => }/DocumentReader.java | 29 +-- .../{indexing => }/IndexingTaskManager.java | 54 +++-- .../pdf/search/{indexing => }/PdfIndexer.java | 203 ++++++++++++------ .../logic/pdf/search/PdfIndexerManager.java | 79 +++++++ .../search/{retrieval => }/PdfSearcher.java | 41 ++-- .../jabref/logic/search/DatabaseSearcher.java | 10 +- .../org/jabref/logic/search/SearchQuery.java | 2 +- .../jabref/logic/util/StandardFileType.java | 3 - .../model/database/BibDatabaseContext.java | 10 +- .../jabref/model/database/BibDatabases.java | 2 +- .../java/org/jabref/model/entry/BibEntry.java | 24 ++- .../org/jabref/model/entry/LinkedFile.java | 19 +- .../search/rules/ContainsBasedSearchRule.java | 6 +- .../search/rules/FullTextSearchRule.java | 45 ++-- .../search/rules/GrammarBasedSearchRule.java | 33 +-- .../architecture/TestArchitectureTest.java | 1 + .../gui/entryeditor/CommentsTabTest.java | 2 +- .../cleanup/FieldFormatterCleanupsTest.java | 39 +++- .../importer/util/MetaDataParserTest.java | 21 ++ .../{indexing => }/DocumentReaderTest.java | 2 +- .../search/{indexing => }/PdfIndexerTest.java | 58 ++--- .../{retrieval => }/PdfSearcherTest.java | 37 ++-- .../logic/search/DatabaseSearcherTest.java | 2 - .../DatabaseSearcherWithBibFilesTest.java | 172 +++++++++++++++ .../jabref/logic/search/SearchQueryTest.java | 4 +- .../database/BibDatabaseContextTest.java | 7 +- .../jabref/model/groups/SearchGroupTest.java | 28 +++ .../org/jabref/logic/search}/.gitignore | 2 + .../org/jabref/logic/search/README.md | 11 + .../org/jabref/logic/search}/empty.bib | 0 .../logic/search/minimal-all-upper-case.pdf} | Bin 15486 -> 14598 bytes .../logic/search/minimal-all-upper-case.tex} | 6 +- .../logic/search/minimal-mixed-case.pdf} | Bin 15117 -> 15117 bytes .../logic/search/minimal-mixed-case.tex} | 4 - .../search/minimal-note-all-upper-case.pdf | Bin 0 -> 15771 bytes .../search/minimal-note-all-upper-case.tex} | 8 +- .../logic/search/minimal-note-mixed-case.pdf | Bin 0 -> 15771 bytes .../logic/search/minimal-note-mixed-case.tex} | 8 +- .../search/minimal-note-sentence-case.pdf | Bin 0 -> 15771 bytes .../search/minimal-note-sentence-case.tex} | 8 +- .../logic/search/minimal-sentence-case.pdf} | Bin 14866 -> 14867 bytes .../logic/search/minimal-sentence-case.tex} | 4 - .../search/test-library-title-casing.bib | 13 ++ .../test-library-with-attached-files.bib | 27 +++ src/test/search/README.md | 9 - src/test/search/resources/minimal-note.pdf | Bin 15754 -> 0 bytes src/test/search/resources/minimal-note2.pdf | Bin 16010 -> 0 bytes src/test/search/resources/minimal1.pdf | Bin 15621 -> 0 bytes src/test/search/resources/test-library-A.bib | 26 --- src/test/search/resources/test-library-B.bib | 21 -- src/test/search/resources/test-library-C.bib | 34 --- src/test/search/resources/test-library-D.bib | 55 ----- 76 files changed, 790 insertions(+), 505 deletions(-) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/DocumentReader.java (87%) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/IndexingTaskManager.java (63%) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/PdfIndexer.java (50%) create mode 100644 src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java rename src/main/java/org/jabref/logic/pdf/search/{retrieval => }/PdfSearcher.java (64%) rename src/test/java/org/jabref/logic/pdf/search/{indexing => }/DocumentReaderTest.java (98%) rename src/test/java/org/jabref/logic/pdf/search/{indexing => }/PdfIndexerTest.java (74%) rename src/test/java/org/jabref/logic/pdf/search/{retrieval => }/PdfSearcherTest.java (76%) create mode 100644 src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java rename src/test/{search/resources => resources/org/jabref/logic/search}/.gitignore (83%) create mode 100644 src/test/resources/org/jabref/logic/search/README.md rename src/test/{search/resources => resources/org/jabref/logic/search}/empty.bib (100%) rename src/test/{search/resources/minimal-note1.pdf => resources/org/jabref/logic/search/minimal-all-upper-case.pdf} (89%) rename src/test/{search/resources/minimal1.tex => resources/org/jabref/logic/search/minimal-all-upper-case.tex} (53%) rename src/test/{search/resources/minimal2.pdf => resources/org/jabref/logic/search/minimal-mixed-case.pdf} (97%) rename src/test/{search/resources/minimal2.tex => resources/org/jabref/logic/search/minimal-mixed-case.tex} (80%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf rename src/test/{search/resources/minimal-note1.tex => resources/org/jabref/logic/search/minimal-note-all-upper-case.tex} (63%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-mixed-case.pdf rename src/test/{search/resources/minimal-note2.tex => resources/org/jabref/logic/search/minimal-note-mixed-case.tex} (63%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.pdf rename src/test/{search/resources/minimal-note.tex => resources/org/jabref/logic/search/minimal-note-sentence-case.tex} (63%) rename src/test/{search/resources/minimal.pdf => resources/org/jabref/logic/search/minimal-sentence-case.pdf} (96%) rename src/test/{search/resources/minimal.tex => resources/org/jabref/logic/search/minimal-sentence-case.tex} (97%) create mode 100644 src/test/resources/org/jabref/logic/search/test-library-title-casing.bib create mode 100644 src/test/resources/org/jabref/logic/search/test-library-with-attached-files.bib delete mode 100644 src/test/search/README.md delete mode 100644 src/test/search/resources/minimal-note.pdf delete mode 100644 src/test/search/resources/minimal-note2.pdf delete mode 100644 src/test/search/resources/minimal1.pdf delete mode 100644 src/test/search/resources/test-library-A.bib delete mode 100644 src/test/search/resources/test-library-B.bib delete mode 100644 src/test/search/resources/test-library-C.bib delete mode 100644 src/test/search/resources/test-library-D.bib diff --git a/CHANGELOG.md b/CHANGELOG.md index 8506cd4d882..ddbd81a149c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,15 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed - The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) +- The index directories for full text search have now more readable names to increase debugging possibilities using Apache Lucense's Lurk. [#10193](https://github.com/JabRef/jabref/issues/10193) +- The fulltext search also indexes files ending with .pdf (but do not having an explicit file type set). [#10193](https://github.com/JabRef/jabref/issues/10193) - We changed the order of the lists in the "Citation relations" tab. `Cites` are now on the left and `Cited by` on the right [#10572](https://github.com/JabRef/jabref/pull/10752) ### Fixed - We fixed an issue where attempting to cancel the importing/generation of an entry from id is ignored. [#10508](https://github.com/JabRef/jabref/issues/10508) - We fixed an issue where the preview panel showing the wrong entry (an entry that is not selected in the entry table). [#9172](https://github.com/JabRef/jabref/issues/9172) +- The last page of a PDF is now indexed by the full text search. [#10193](https://github.com/JabRef/jabref/issues/10193) - We fixed an issue where the duplicate check did not take umlauts or other LaTeX-encoded characters into account. [#10744](https://github.com/JabRef/jabref/pull/10744) - We fixed the colors of the icon on hover for unset special fields. [#10431](https://github.com/JabRef/jabref/issues/10431) diff --git a/src/main/java/org/jabref/gui/JabRefExecutorService.java b/src/main/java/org/jabref/gui/JabRefExecutorService.java index b23beeb8d2c..90c3b31bdae 100644 --- a/src/main/java/org/jabref/gui/JabRefExecutorService.java +++ b/src/main/java/org/jabref/gui/JabRefExecutorService.java @@ -13,6 +13,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.jabref.logic.pdf.search.PdfIndexerManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,6 +148,8 @@ public void shutdownEverything() { gracefullyShutdown(this.executorService); gracefullyShutdown(this.lowPriorityExecutorService); + PdfIndexerManager.shutdownAllIndexers(); + timer.cancel(); } diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 926dc41d6ef..0631c7fedc5 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -61,6 +61,7 @@ import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.undo.AddUndoableActionEvent; import org.jabref.logic.undo.UndoChangeEvent; @@ -862,6 +863,7 @@ public void closeTab(LibraryTab libraryTab) { } AutosaveManager.shutdown(context); BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); + PdfIndexerManager.shutdownAllIndexers(); } private void removeTab(LibraryTab libraryTab) { diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index 134ff626773..657429b2e3c 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -262,7 +262,7 @@ private void openDatabases() { for (int tabNumber = 0; tabNumber < parserResults.size(); tabNumber++) { // ToDo: Method needs to be rewritten, because the index of the parser result and of the libraryTab may not // be identical, if there are also other tabs opened, that are not libraryTabs. Currently there are none, - // therefor for now this ok. + // therefore for now this ok. ParserResult pr = parserResults.get(tabNumber); if (pr.hasWarnings()) { ParserResultWarningDialog.showParserResultWarningDialog(pr, mainFrame.getDialogService()); diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 341476d2098..658117d8483 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -51,8 +51,9 @@ import org.jabref.logic.importer.util.FileFieldParser; import org.jabref.logic.l10n.Localization; import org.jabref.logic.pdf.FileAnnotationCache; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.pdf.search.IndexingTaskManager; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.search.SearchQuery; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.util.UpdateField; @@ -233,7 +234,7 @@ public void onDatabaseLoadingSucceed(ParserResult result) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - indexingTaskManager.updateIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); + indexingTaskManager.updateIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); } catch (IOException e) { LOGGER.error("Cannot access lucene index", e); } @@ -912,10 +913,8 @@ private class IndexUpdateListener { public void listen(EntriesAddedEvent addedEntryEvent) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); - for (BibEntry addedEntry : addedEntryEvent.getBibEntries()) { - indexingTaskManager.addToIndex(pdfIndexer, addedEntry, bibDatabaseContext); - } + PdfIndexer pdfIndexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); + indexingTaskManager.addToIndex(pdfIndexer, addedEntryEvent.getBibEntries()); } catch (IOException e) { LOGGER.error("Cannot access lucene index", e); } @@ -926,7 +925,7 @@ public void listen(EntriesAddedEvent addedEntryEvent) { public void listen(EntriesRemovedEvent removedEntriesEvent) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); + PdfIndexer pdfIndexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); for (BibEntry removedEntry : removedEntriesEvent.getBibEntries()) { indexingTaskManager.removeFromIndex(pdfIndexer, removedEntry); } @@ -949,8 +948,9 @@ public void listen(FieldChangedEvent fieldChangedEvent) { removedFiles.remove(newFileList); try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), fieldChangedEvent.getBibEntry(), addedFiles, bibDatabaseContext); - indexingTaskManager.removeFromIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), fieldChangedEvent.getBibEntry(), removedFiles); + PdfIndexer indexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); + indexingTaskManager.addToIndex(indexer, fieldChangedEvent.getBibEntry(), addedFiles); + indexingTaskManager.removeFromIndex(indexer, removedFiles); } catch (IOException e) { LOGGER.warn("I/O error when writing lucene index", e); } diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index f1ff42c5785..743ad0ef473 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import javafx.beans.Observable; import javafx.beans.binding.Bindings; @@ -31,7 +30,6 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; -import org.jabref.model.util.OptionalUtil; import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyBinding; @@ -144,11 +142,6 @@ public void setActiveDatabase(BibDatabaseContext database) { } } - public List getEntriesInCurrentDatabase() { - return OptionalUtil.flatMap(activeDatabase.get(), BibDatabaseContext::getEntries) - .collect(Collectors.toList()); - } - public void clearSearchQuery() { activeSearchQuery.setValue(Optional.empty()); } diff --git a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java index dc4434ce5d4..4aa92a3520d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java @@ -25,7 +25,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java index ca15fe49b7c..a66339ea7b6 100644 --- a/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java @@ -17,7 +17,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java index bdaf404b4c1..20e54396bfe 100644 --- a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java @@ -34,7 +34,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java index da37540e513..c63c145cfba 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java @@ -9,7 +9,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java index d511ef268cf..ad522d2411f 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java @@ -9,7 +9,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java index 2bfdc10b92b..5f61a0279ab 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java @@ -16,7 +16,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java index ed388027b69..4ed6b1434ad 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java @@ -20,7 +20,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java b/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java index 1555f577c45..648cbe003a8 100644 --- a/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java @@ -7,7 +7,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java index fb856f62f5e..9e7d03eae64 100644 --- a/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java @@ -16,7 +16,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryType; diff --git a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java index ce94dcca516..064855d593d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java @@ -13,7 +13,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java index 87cb097b734..954fabcf47f 100644 --- a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java +++ b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java @@ -34,6 +34,7 @@ import org.jabref.logic.exporter.SelfContainedSaveConfiguration; import org.jabref.logic.l10n.Encodings; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.shared.prefs.SharedDatabasePreferences; import org.jabref.logic.util.StandardFileType; @@ -137,13 +138,12 @@ public void saveSelectedAsPlain() { boolean saveAs(Path file, SaveDatabaseMode mode) { BibDatabaseContext context = libraryTab.getBibDatabaseContext(); - // Close AutosaveManager and BackupManager for original library Optional databasePath = context.getDatabasePath(); if (databasePath.isPresent()) { - final Path oldFile = databasePath.get(); - context.setDatabasePath(oldFile); + // Close AutosaveManager, BackupManager, and PdfIndexer for original library AutosaveManager.shutdown(context); BackupManager.shutdown(context, this.preferences.getFilePreferences().getBackupDirectory(), preferences.getFilePreferences().shouldCreateBackup()); + PdfIndexerManager.shutdownIndexer(context); } // Set new location @@ -164,6 +164,7 @@ boolean saveAs(Path file, SaveDatabaseMode mode) { // Reset (here: uninstall and install again) AutosaveManager and BackupManager for the new file name libraryTab.resetChangeMonitor(); libraryTab.installAutosaveManagerAndBackupManager(); + // PdfIndexerManager does not need to be called; the method {@link org.jabref.logic.pdf.search.PdfIndexerManager.get()} is called if a new indexer is needed preferences.getGuiPreferences().getFileHistory().newFile(file); } diff --git a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java index 57df1e395c8..8e15ff4c474 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java +++ b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java @@ -15,8 +15,8 @@ import org.jabref.logic.cleanup.MoveFilesCleanup; import org.jabref.logic.cleanup.RenamePdfCleanup; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.pdf.search.IndexingTaskManager; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.util.io.FileNameCleaner; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; @@ -87,7 +87,7 @@ public void moveFilesToFileDirRenameAndAddToEntry(BibEntry entry, List fil } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, filePreferences), entry); } catch (IOException e) { LOGGER.error("Could not access Fulltext-Index", e); } @@ -105,9 +105,9 @@ public void copyFilesToFileDirAndAddToEntry(BibEntry entry, List files, In } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, filePreferences), entry); } catch (IOException e) { - LOGGER.error("Could not access Fulltext-Index", e); + LOGGER.error("Could not access fulltext index", e); } } diff --git a/src/main/java/org/jabref/gui/preview/PreviewPanel.java b/src/main/java/org/jabref/gui/preview/PreviewPanel.java index 3a332dba0c7..e2c6d3c4ada 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewPanel.java +++ b/src/main/java/org/jabref/gui/preview/PreviewPanel.java @@ -25,7 +25,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.logic.preview.PreviewLayout; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java index 4fb2a575ab6..3a686979b35 100644 --- a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java +++ b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java @@ -9,7 +9,8 @@ import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; @@ -74,8 +75,8 @@ private void rebuildIndex() { return; } try { - currentLibraryTab.get().getIndexingTaskManager().createIndex(PdfIndexer.of(databaseContext, filePreferences)); - currentLibraryTab.get().getIndexingTaskManager().updateIndex(PdfIndexer.of(databaseContext, filePreferences), databaseContext); + PdfIndexer indexer = PdfIndexerManager.getIndexer(databaseContext, filePreferences); + currentLibraryTab.get().getIndexingTaskManager().rebuildIndex(indexer); } catch (IOException e) { dialogService.notify(Localization.lang("Failed to access fulltext search index")); LOGGER.error("Failed to access fulltext search index", e); diff --git a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java index bb9651f9145..e37274328c0 100644 --- a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java +++ b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java @@ -204,13 +204,16 @@ public static FieldFormatterCleanups parse(List formatterMetaList) { } static Formatter getFormatterFromString(String formatterName) { - for (Formatter formatter : Formatters.getAll()) { - if (formatterName.equals(formatter.getKey())) { - return formatter; - } - } - LOGGER.info("Formatter {} not found.", formatterName); - return new IdentityFormatter(); + return Formatters + .getFormatterForKey(formatterName) + .orElseGet(() -> { + if (!"identity".equals(formatterName)) { + // The identity formatter is not listed in the formatters list, but is still valid + // Therefore, we log errors in other cases only + LOGGER.info("Formatter {} not found.", formatterName); + } + return new IdentityFormatter(); + }); } @Override diff --git a/src/main/java/org/jabref/logic/formatter/Formatters.java b/src/main/java/org/jabref/logic/formatter/Formatters.java index ad57b7f6202..82fb14932e8 100644 --- a/src/main/java/org/jabref/logic/formatter/Formatters.java +++ b/src/main/java/org/jabref/logic/formatter/Formatters.java @@ -3,9 +3,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.jabref.logic.cleanup.Formatter; import org.jabref.logic.formatter.bibtexfields.CleanupUrlFormatter; @@ -40,6 +42,12 @@ public class Formatters { private static final Pattern TRUNCATE_PATTERN = Pattern.compile("\\Atruncate\\d+\\z"); + private static Map keyToFormatterMap; + + static { + keyToFormatterMap = getAll().stream().collect(Collectors.toMap(Formatter::getKey, f -> f)); + } + private Formatters() { } @@ -92,6 +100,11 @@ public static List getAll() { return all; } + public static Optional getFormatterForKey(String name) { + Objects.requireNonNull(name); + return keyToFormatterMap.containsKey(name) ? Optional.of(keyToFormatterMap.get(name)) : Optional.empty(); + } + public static Optional getFormatterForModifier(String modifier) { Objects.requireNonNull(modifier); @@ -115,7 +128,7 @@ public static Optional getFormatterForModifier(String modifier) { int truncateAfter = Integer.parseInt(modifier.substring(8)); return Optional.of(new TruncateFormatter(truncateAfter)); } else { - return getAll().stream().filter(f -> f.getKey().equals(modifier)).findAny(); + return getFormatterForKey(modifier); } } } 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 a609d68ca8d..3057071e82a 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -301,6 +301,8 @@ private void parseJabRefComment(Map meta) { return; } + // We remove all line breaks in the metadata + // These have been inserted to prevent too long lines when the file was saved, and are not part of the data. String comment = buffer.toString().replaceAll("[\\x0d\\x0a]", ""); if (comment.substring(0, Math.min(comment.length(), MetaData.META_FLAG.length())).equals(MetaData.META_FLAG)) { if (comment.startsWith(MetaData.META_FLAG)) { @@ -309,10 +311,6 @@ private void parseJabRefComment(Map meta) { int pos = rest.indexOf(':'); if (pos > 0) { - // We remove all line breaks in the metadata - these - // will have been inserted - // to prevent too long lines when the file was - // saved, and are not part of the data. meta.put(rest.substring(0, pos), rest.substring(pos + 1)); // meta comments are always re-written by JabRef and not stored in the file diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index b5b733f7973..cccf64afe31 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -78,6 +78,8 @@ public MetaData parse(Map data, Character keywordSeparator) thro /** * Parses the data map and changes the given {@link MetaData} instance respectively. + * + * @return the given metaData instance (which is modified, too) */ public MetaData parse(MetaData metaData, Map data, Character keywordSeparator) throws ParseException { List defaultCiteKeyPattern = new ArrayList<>(); @@ -103,7 +105,7 @@ public MetaData parse(MetaData metaData, Map data, Character key String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1); metaData.setUserFileDirectory(user, parseDirectory(entry.getValue())); } else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY_LATEX)) { - // The user name starts directly after FILE_DIRECTORY_LATEX" + '-' + // The user name starts directly after FILE_DIRECTORY_LATEX + '-' String user = entry.getKey().substring(MetaData.FILE_DIRECTORY_LATEX.length() + 1); Path path = Path.of(parseDirectory(entry.getValue())).normalize(); metaData.setLatexFileDirectory(user, path); diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java b/src/main/java/org/jabref/logic/pdf/search/DocumentReader.java similarity index 87% rename from src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java rename to src/main/java/org/jabref/logic/pdf/search/DocumentReader.java index e2e1c0f5b7e..8431db88b52 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java +++ b/src/main/java/org/jabref/logic/pdf/search/DocumentReader.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Files; @@ -57,7 +57,7 @@ public final class DocumentReader { public DocumentReader(BibEntry bibEntry, FilePreferences filePreferences) { this.filePreferences = filePreferences; if (bibEntry.getFiles().isEmpty()) { - throw new IllegalStateException("There are no linked PDF files to this BibEntry!"); + throw new IllegalStateException("There are no linked PDF files to this BibEntry."); } this.entry = bibEntry; @@ -93,17 +93,17 @@ public List readLinkedPdfs(BibDatabaseContext databaseContext) { private List readPdfContents(LinkedFile pdf, Path resolvedPdfPath) { List pages = new ArrayList<>(); try (PDDocument pdfDocument = Loader.loadPDF(resolvedPdfPath.toFile())) { - for (int pageNumber = 0; pageNumber < pdfDocument.getNumberOfPages(); pageNumber++) { - Document newDocument = new Document(); - addIdentifiers(newDocument, pdf.getLink()); - addMetaData(newDocument, resolvedPdfPath, pageNumber); - try { - addContentIfNotEmpty(pdfDocument, newDocument, pageNumber); - } catch (IOException e) { - LOGGER.warn("Could not read page {} of {}", pageNumber, resolvedPdfPath.toAbsolutePath(), e); - } - pages.add(newDocument); + for (int pageNumber = 0; pageNumber < pdfDocument.getNumberOfPages(); pageNumber++) { + Document newDocument = new Document(); + addIdentifiers(newDocument, pdf.getLink()); + addMetaData(newDocument, resolvedPdfPath, pageNumber); + try { + addContentIfNotEmpty(pdfDocument, newDocument, pageNumber); + } catch (IOException e) { + LOGGER.warn("Could not read page {} of {}", pageNumber, resolvedPdfPath.toAbsolutePath(), e); } + pages.add(newDocument); + } } catch (IOException e) { LOGGER.warn("Could not read {}", resolvedPdfPath.toAbsolutePath(), e); } @@ -145,8 +145,9 @@ public static String mergeLines(String text) { private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument, int pageNumber) throws IOException { PDFTextStripper pdfTextStripper = new PDFTextStripper(); pdfTextStripper.setLineSeparator("\n"); - pdfTextStripper.setStartPage(pageNumber); - pdfTextStripper.setEndPage(pageNumber); + // Apache PDFTextStripper is 1-based. See {@link org.apache.pdfbox.text.PDFTextStripper.processPages} + pdfTextStripper.setStartPage(pageNumber + 1); + pdfTextStripper.setEndPage(pageNumber + 1); String pdfContent = pdfTextStripper.getText(pdfDocument); if (StringUtil.isNotBlank(pdfContent)) { diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java b/src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java similarity index 63% rename from src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java rename to src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java index ba8bc5e5d12..8ae4117e72d 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java +++ b/src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java @@ -1,9 +1,11 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.DefaultTaskExecutor; @@ -85,41 +87,47 @@ public AutoCloseable blockNewTasks() { }; } - public void createIndex(PdfIndexer indexer) { - enqueueTask(indexer::createIndex); + public void rebuildIndex(PdfIndexer indexer) { + enqueueTask(indexer::rebuildIndex); } + /** + * Updates the index by performing a delta analysis of the files already existing in the index and the files in the library. + */ public void updateIndex(PdfIndexer indexer, BibDatabaseContext databaseContext) { Set pathsToRemove = indexer.getListOfFilePaths(); - for (BibEntry entry : databaseContext.getEntries()) { - for (LinkedFile file : entry.getFiles()) { - enqueueTask(() -> indexer.addToIndex(entry, file, databaseContext)); - pathsToRemove.remove(file.getLink()); - } - } - for (String pathToRemove : pathsToRemove) { - enqueueTask(() -> indexer.removeFromIndex(pathToRemove)); - } + databaseContext.getEntries().stream() + .flatMap(entry -> entry.getFiles().stream()) + .map(LinkedFile::getLink) + .forEach(pathsToRemove::remove); + // The indexer checks the attached PDFs for modifications (based on the timestamp of the PDF) and reindexes the PDF if it is newer than the index. Therefore, we need to pass the whole library to the indexer for re-indexing. + addToIndex(indexer, databaseContext.getEntries()); + enqueueTask(() -> indexer.removePathsFromIndex(pathsToRemove)); } - public void addToIndex(PdfIndexer indexer, BibEntry entry, BibDatabaseContext databaseContext) { - addToIndex(indexer, entry, entry.getFiles(), databaseContext); + public void addToIndex(PdfIndexer indexer, List entries) { + AtomicInteger counter = new AtomicInteger(); + // To enable seeing progress in the UI, we group the entries in chunks of 50 + // Solution inspired by https://stackoverflow.com/a/27595803/873282 + entries.stream().collect(Collectors.groupingBy(x -> counter.getAndIncrement() / 50)) + .values() + .forEach(list -> enqueueTask(() -> indexer.addToIndex(list))); } - public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.addToIndex(entry, file, databaseContext)); - } + public void addToIndex(PdfIndexer indexer, BibEntry entry) { + enqueueTask(() -> indexer.addToIndex(entry)); } - public void removeFromIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.removeFromIndex(file.getLink())); - } + public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { + enqueueTask(() -> indexer.addToIndex(entry, linkedFiles)); } public void removeFromIndex(PdfIndexer indexer, BibEntry entry) { - enqueueTask(() -> removeFromIndex(indexer, entry, entry.getFiles())); + enqueueTask(() -> indexer.removeFromIndex(entry)); + } + + public void removeFromIndex(PdfIndexer indexer, List linkedFiles) { + enqueueTask(() -> indexer.removeFromIndex(linkedFiles)); } public void updateDatabaseName(String name) { diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java similarity index 50% rename from src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java rename to src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java index d10a6c0221a..a973831e134 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java @@ -1,16 +1,16 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.jabref.gui.LibraryTab; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -19,6 +19,7 @@ import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.preferences.FilePreferences; +import com.google.common.annotations.VisibleForTesting; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexNotFoundException; @@ -41,75 +42,138 @@ */ public class PdfIndexer { - private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PdfIndexer.class); - private final Directory directoryToIndex; - private BibDatabaseContext databaseContext; + @VisibleForTesting + IndexWriter indexWriter; + private final BibDatabaseContext databaseContext; private final FilePreferences filePreferences; + private final Directory indexDirectory; + private IndexReader reader; - public PdfIndexer(Directory indexDirectory, FilePreferences filePreferences) { - this.directoryToIndex = indexDirectory; + private PdfIndexer(BibDatabaseContext databaseContext, Directory indexDirectory, FilePreferences filePreferences) { + this.databaseContext = databaseContext; + this.indexDirectory = indexDirectory; this.filePreferences = filePreferences; } + /** + * Method is public, because DatabaseSearcherWithBibFilesTest resides in another package + */ + @VisibleForTesting + public static PdfIndexer of(BibDatabaseContext databaseContext, Path indexDirectory, FilePreferences filePreferences) throws IOException { + return new PdfIndexer(databaseContext, new NIOFSDirectory(indexDirectory), filePreferences); + } + + /** + * Method is public, because DatabaseSearcherWithBibFilesTest resides in another package + */ + @VisibleForTesting public static PdfIndexer of(BibDatabaseContext databaseContext, FilePreferences filePreferences) throws IOException { - return new PdfIndexer(new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); + return new PdfIndexer(databaseContext, new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); } /** - * Adds all PDF files linked to an entry in the database to new Lucene search index. Any previous state of the - * Lucene search index will be deleted! + * Creates (and thus resets) the PDF index. No re-indexing will be done. + * Any previous state of the Lucene search is deleted. */ public void createIndex() { - // Create new index by creating IndexWriter but not writing anything. - try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, new IndexWriterConfig(new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE))) { - // empty comment for checkstyle + LOGGER.debug("Creating new index for directory {}.", indexDirectory); + initializeIndexWriterAndReader(IndexWriterConfig.OpenMode.CREATE); + } + + /** + * Needs to be accessed by {@link PdfSearcher} + */ + IndexWriter getIndexWriter() { + LOGGER.trace("Getting the index writer"); + if (indexWriter == null) { + LOGGER.trace("Initializing the index writer"); + initializeIndexWriterAndReader(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); + } else { + LOGGER.trace("Using existing index writer"); + } + return indexWriter; + } + + private void initializeIndexWriterAndReader(IndexWriterConfig.OpenMode mode) { + try { + indexWriter = new IndexWriter( + indexDirectory, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(mode)); + } catch (IOException e) { + LOGGER.error("Could not initialize the IndexWriter", e); + } + try { + reader = DirectoryReader.open(indexWriter); } catch (IOException e) { - LOGGER.warn("Could not create new Index!", e); + LOGGER.error("Could not initialize the IndexReader", e); } } - public void addToIndex(BibDatabaseContext databaseContext) { - for (BibEntry entry : databaseContext.getEntries()) { - addToIndex(entry, databaseContext); + /** + * Rebuilds the PDF index. All PDF files linked to entries in the database will be re-indexed. + */ + public void rebuildIndex() { + LOGGER.debug("Rebuilding index."); + createIndex(); + addToIndex(databaseContext.getEntries()); + } + + public void addToIndex(List entries) { + int count = 0; + for (BibEntry entry : entries) { + addToIndex(entry, false); + count++; + if (count % 100 == 0) { + doCommit(); + } } + doCommit(); + LOGGER.debug("Added {} documents to the index.", count); } /** - * Adds all the pdf files linked to one entry in the database to an existing (or new) Lucene search index + * Adds all PDF files linked to one entry in the database to an existing (or new) Lucene search index * * @param entry a bibtex entry to link the pdf files to - * @param databaseContext the associated BibDatabaseContext */ - public void addToIndex(BibEntry entry, BibDatabaseContext databaseContext) { - addToIndex(entry, entry.getFiles(), databaseContext); + public void addToIndex(BibEntry entry) { + addToIndex(entry, entry.getFiles(), true); + } + + private void addToIndex(BibEntry entry, boolean shouldCommit) { + addToIndex(entry, entry.getFiles(), false); + if (shouldCommit) { + doCommit(); + } } /** * Adds a list of pdf files linked to one entry in the database to an existing (or new) Lucene search index * * @param entry a bibtex entry to link the pdf files to - * @param databaseContext the associated BibDatabaseContext */ - public void addToIndex(BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { + public void addToIndex(BibEntry entry, Collection linkedFiles) { + addToIndex(entry, linkedFiles, true); + } + + public void addToIndex(BibEntry entry, Collection linkedFiles, boolean shouldCommit) { for (LinkedFile linkedFile : linkedFiles) { - addToIndex(entry, linkedFile, databaseContext); + addToIndex(entry, linkedFile, false); + } + if (shouldCommit) { + doCommit(); } } - /** - * Adds a pdf file linked to one entry in the database to an existing (or new) Lucene search index - * - * @param entry a bibtex entry - * @param linkedFile the link to the pdf files - */ - public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext databaseContext) { - if (databaseContext != null) { - this.databaseContext = databaseContext; - } - if (!entry.getFiles().isEmpty()) { - addToIndex(entry, linkedFile); + private void doCommit() { + try { + getIndexWriter().commit(); + } catch (IOException e) { + LOGGER.warn("Could not commit changes to the index.", e); } } @@ -119,14 +183,11 @@ public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext * @param linkedFilePath the path to the file to be removed */ public void removeFromIndex(String linkedFilePath) { - try (IndexWriter indexWriter = new IndexWriter( - directoryToIndex, - new IndexWriterConfig( - new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { - indexWriter.deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFilePath)); - indexWriter.commit(); + try { + getIndexWriter().deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFilePath)); + getIndexWriter().commit(); } catch (IOException e) { - LOGGER.warn("Could not initialize the IndexWriter!", e); + LOGGER.debug("Could not remove document {} from the index.", linkedFilePath, e); } } @@ -136,30 +197,21 @@ public void removeFromIndex(String linkedFilePath) { * @param entry the entry documents are linked to */ public void removeFromIndex(BibEntry entry) { - removeFromIndex(entry, entry.getFiles()); + removeFromIndex(entry.getFiles()); } /** * Removes a list of files linked to a bib-entry from the index - * - * @param entry the entry documents are linked to */ - public void removeFromIndex(BibEntry entry, List linkedFiles) { + public void removeFromIndex(Collection linkedFiles) { for (LinkedFile linkedFile : linkedFiles) { removeFromIndex(linkedFile.getLink()); } } - /** - * Deletes all entries from the Lucene search index. - */ - public void flushIndex() { - IndexWriterConfig config = new IndexWriterConfig(); - config.setOpenMode(IndexWriterConfig.OpenMode.CREATE); - try (IndexWriter deleter = new IndexWriter(directoryToIndex, config)) { - // Do nothing. Index is deleted. - } catch (IOException e) { - LOGGER.warn("The IndexWriter could not be initialized", e); + public void removePathsFromIndex(Collection linkedFiles) { + for (String linkedFile : linkedFiles) { + removeFromIndex(linkedFile); } } @@ -170,8 +222,15 @@ public void flushIndex() { * @param entry the entry associated with the file * @param linkedFile the file to write to the index */ - private void addToIndex(BibEntry entry, LinkedFile linkedFile) { - if (linkedFile.isOnlineLink() || !StandardFileType.PDF.getName().equals(linkedFile.getFileType())) { + public void addToIndex(BibEntry entry, LinkedFile linkedFile) { + this.addToIndex(entry, linkedFile, true); + } + + private void addToIndex(BibEntry entry, LinkedFile linkedFile, boolean shouldCommit) { + if (linkedFile.isOnlineLink() || + (!StandardFileType.PDF.getName().equals(linkedFile.getFileType()) && + // We do not require the file type to be set + (!linkedFile.getLink().endsWith(".pdf") && !linkedFile.getLink().endsWith(".PDF")))) { return; } Optional resolvedPath = linkedFile.findIn(databaseContext, filePreferences); @@ -182,7 +241,7 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { LOGGER.debug("Adding {} to index", linkedFile.getLink()); try { // Check if a document with this path is already in the index - try (IndexReader reader = DirectoryReader.open(directoryToIndex)) { + try { IndexSearcher searcher = new IndexSearcher(reader); TermQuery query = new TermQuery(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); TopDocs topDocs = searcher.search(query, 1); @@ -190,28 +249,27 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { if (topDocs.scoreDocs.length > 0) { Document doc = reader.document(topDocs.scoreDocs[0].doc); long indexModificationTime = Long.parseLong(doc.getField(SearchFieldConstants.MODIFIED).stringValue()); - BasicFileAttributes attributes = Files.readAttributes(resolvedPath.get(), BasicFileAttributes.class); - if (indexModificationTime >= attributes.lastModifiedTime().to(TimeUnit.SECONDS)) { + LOGGER.debug("File {} is already indexed", linkedFile.getLink()); return; } } } catch (IndexNotFoundException e) { - // if there is no index yet, don't need to check anything! + LOGGER.debug("Index not found. Continuing.", e); } // If no document was found, add the new one Optional> pages = new DocumentReader(entry, filePreferences).readLinkedPdf(this.databaseContext, linkedFile); if (pages.isPresent()) { - try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, - new IndexWriterConfig( - new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { - indexWriter.addDocuments(pages.get()); - indexWriter.commit(); + getIndexWriter().addDocuments(pages.get()); + if (shouldCommit) { + getIndexWriter().commit(); } + } else { + LOGGER.debug("No content found in file {}", linkedFile.getLink()); } } catch (IOException e) { - LOGGER.warn("Could not add the document {} to the index!", linkedFile.getLink(), e); + LOGGER.warn("Could not add document {} to the index.", linkedFile.getLink(), e); } } @@ -222,7 +280,7 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { */ public Set getListOfFilePaths() { Set paths = new HashSet<>(); - try (IndexReader reader = DirectoryReader.open(directoryToIndex)) { + try (IndexReader reader = DirectoryReader.open(indexWriter)) { IndexSearcher searcher = new IndexSearcher(reader); MatchAllDocsQuery query = new MatchAllDocsQuery(); TopDocs allDocs = searcher.search(query, Integer.MAX_VALUE); @@ -231,8 +289,13 @@ public Set getListOfFilePaths() { paths.add(doc.getField(SearchFieldConstants.PATH).stringValue()); } } catch (IOException e) { + LOGGER.debug("Could not read from index. Returning intermediate result.", e); return paths; } return paths; } + + public void close() throws IOException { + indexWriter.close(); + } } diff --git a/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java new file mode 100644 index 00000000000..3f4bca38c49 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java @@ -0,0 +1,79 @@ +package org.jabref.logic.pdf.search; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.FilePreferences; + +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A PdfIndexer takes a long time to build. Caching it speeds up. + *

+ * The PdfIndexer is related to the BibDatabaseContext and the FilePreferences. If the user changes the path of the library + * or the file preferences, we need to create a new PdfIndexer. Otherwise, we can reuse the existing one. + *

+ * This manager implements a Object Pool pattern for {@link PdfIndexer}. + */ +public class PdfIndexerManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(PdfIndexerManager.class); + + // Map from the path of the library index to the respective indexer + private static Map indexerMap = new HashMap<>(); + + // We store the file preferences for each path, so that we can update the indexer when the preferences change + private static Map pathFilePreferencesMap = new HashMap<>(); + + public static @NonNull PdfIndexer getIndexer(BibDatabaseContext context, FilePreferences filePreferences) throws IOException { + Path fulltextIndexPath = context.getFulltextIndexPath(); + PdfIndexer indexer = indexerMap.get(fulltextIndexPath); + if (indexer != null) { + // Check if the file preferences have changed + FilePreferences storedFilePreferences = pathFilePreferencesMap.get(fulltextIndexPath); + if (storedFilePreferences.equals(filePreferences)) { + LOGGER.trace("Found existing indexer for context {}", context); + return indexer; + } + LOGGER.debug("File preferences have changed, updating indexer"); + indexer.close(); + indexer = PdfIndexer.of(context, filePreferences); + indexerMap.put(fulltextIndexPath, indexer); + pathFilePreferencesMap.put(fulltextIndexPath, filePreferences); + return indexer; + } + LOGGER.debug("No indexer found for context {}, creating new one", context); + indexer = PdfIndexer.of(context, filePreferences); + indexerMap.put(fulltextIndexPath, indexer); + pathFilePreferencesMap.put(fulltextIndexPath, filePreferences); + return indexer; + } + + public static void shutdownAllIndexers() { + indexerMap.values().forEach(indexer -> { + try { + indexer.close(); + } catch (Exception e) { + LOGGER.debug("Problem closing PDF indexer", e); + } + }); + } + + public static void shutdownIndexer(BibDatabaseContext context) { + PdfIndexer indexer = indexerMap.get(context.getFulltextIndexPath()); + if (indexer != null) { + try { + indexer.close(); + } catch (IOException e) { + LOGGER.debug("Could not close indexer", e); + } + } else { + LOGGER.debug("No indexer found for context {}", context); + } + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java b/src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java similarity index 64% rename from src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java rename to src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java index 2fd7e54e5b0..3906341ecb5 100644 --- a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java @@ -1,12 +1,11 @@ -package org.jabref.logic.pdf.search.retrieval; +package org.jabref.logic.pdf.search; import java.io.IOException; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.jabref.gui.LibraryTab; -import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.pdf.search.EnglishStemAnalyzer; import org.jabref.model.pdf.search.PdfSearchResults; import org.jabref.model.pdf.search.SearchResult; @@ -20,8 +19,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.store.Directory; -import org.apache.lucene.store.NIOFSDirectory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,14 +28,15 @@ public final class PdfSearcher { private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - private final Directory indexDirectory; + private final PdfIndexer indexer; + private EnglishStemAnalyzer englishStemAnalyzer = new EnglishStemAnalyzer(); - private PdfSearcher(Directory indexDirectory) { - this.indexDirectory = indexDirectory; + private PdfSearcher(PdfIndexer indexer) { + this.indexer = indexer; } - public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOException { - return new PdfSearcher(new NIOFSDirectory(databaseContext.getFulltextIndexPath())); + public static PdfSearcher of(PdfIndexer indexer) throws IOException { + return new PdfSearcher(indexer); } /** @@ -48,32 +46,27 @@ public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOExcept * @param maxHits number of maximum search results, must be positive * @return a result set of all documents that have matches in any fields */ - public PdfSearchResults search(final String searchString, final int maxHits) - throws IOException { - if (StringUtil.isBlank(Objects.requireNonNull(searchString, "The search string was null!"))) { + public PdfSearchResults search(final String searchString, final int maxHits) throws IOException { + if (StringUtil.isBlank(Objects.requireNonNull(searchString, "The search string was null."))) { return new PdfSearchResults(); } if (maxHits <= 0) { - throw new IllegalArgumentException("Must be called with at least 1 maxHits, was" + maxHits); + throw new IllegalArgumentException("Must be called with at least 1 maxHits, was " + maxHits); } - List resultDocs = new LinkedList<>(); - - if (!DirectoryReader.indexExists(indexDirectory)) { - LOGGER.debug("Index directory {} does not yet exist", indexDirectory); - return new PdfSearchResults(); - } - - try (IndexReader reader = DirectoryReader.open(indexDirectory)) { + List resultDocs = new ArrayList<>(); + // We need to point the DirectoryReader to the indexer, because we get errors otherwise + // Hint from https://stackoverflow.com/a/63673753/873282. + try (IndexReader reader = DirectoryReader.open(indexer.getIndexWriter())) { + Query query = new MultiFieldQueryParser(PDF_FIELDS, englishStemAnalyzer).parse(searchString); IndexSearcher searcher = new IndexSearcher(reader); - Query query = new MultiFieldQueryParser(PDF_FIELDS, new EnglishStemAnalyzer()).parse(searchString); TopDocs results = searcher.search(query, maxHits); for (ScoreDoc scoreDoc : results.scoreDocs) { resultDocs.add(new SearchResult(searcher, query, scoreDoc)); } return new PdfSearchResults(resultDocs); } catch (ParseException e) { - LOGGER.warn("Could not parse query: '{}'!\n{}", searchString, e.getMessage()); + LOGGER.warn("Could not parse query: '{}'", searchString, e); return new PdfSearchResults(); } } diff --git a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java index 422d8d4d35c..d5db1b6309f 100644 --- a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java +++ b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java @@ -3,7 +3,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabases; @@ -24,15 +23,18 @@ public DatabaseSearcher(SearchQuery query, BibDatabase database) { this.database = Objects.requireNonNull(database); } + /** + * @return The matches in the order they appear in the library. + */ public List getMatches() { - LOGGER.debug("Search term: " + query); + LOGGER.debug("Search term: {}", query); if (!query.isValid()) { - LOGGER.warn("Search failed: illegal search expression"); + LOGGER.warn("Search failed: invalid search expression"); return Collections.emptyList(); } - List matchEntries = database.getEntries().stream().filter(query::isMatch).collect(Collectors.toList()); + List matchEntries = database.getEntries().stream().filter(query::isMatch).toList(); return BibDatabases.purgeEmptyEntries(matchEntries); } } diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index 4dc9fd40a58..ef1bb0479c3 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -69,7 +69,7 @@ public SearchQuery(String query, EnumSet searchFlags) { @Override public String toString() { - return String.format("\"%s\" (%s, %s)", getQuery(), getCaseSensitiveDescription(), getRegularExpressionDescription()); + return String.format("\"%s\" (%s, %s) %s", getQuery(), getCaseSensitiveDescription(), getRegularExpressionDescription(), searchFlags); } @Override diff --git a/src/main/java/org/jabref/logic/util/StandardFileType.java b/src/main/java/org/jabref/logic/util/StandardFileType.java index d71f5b50dec..904212a5d2a 100644 --- a/src/main/java/org/jabref/logic/util/StandardFileType.java +++ b/src/main/java/org/jabref/logic/util/StandardFileType.java @@ -47,9 +47,6 @@ public enum StandardFileType implements FileType { CITAVI("Citavi", "ctv6bak", "ctv5bak"), MARKDOWN("Markdown", "md"); - - - private final List extensions; private final String name; diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 12d67a71a2f..55869680353 100644 --- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -16,6 +16,7 @@ import org.jabref.logic.shared.DatabaseSynchronizer; import org.jabref.logic.util.CoarseChangeFilter; import org.jabref.logic.util.OS; +import org.jabref.logic.util.io.BackupFileUtil; import org.jabref.model.entry.BibEntry; import org.jabref.model.metadata.MetaData; import org.jabref.model.study.Study; @@ -161,7 +162,7 @@ public List getFileDirectories(FilePreferences preferences) { metaData.getDefaultFileDirectory() .ifPresent(metaDataDirectory -> fileDirs.add(getFileDirectoryPath(metaDataDirectory))); - // 3. BIB file directory or Main file directory + // 3. BIB file directory or main file directory // fileDirs.isEmpty in the case, 1) no user-specific file directory and 2) no general file directory is set // (in the metadata of the bib file) if (fileDirs.isEmpty() && preferences.shouldStoreFilesRelativeToBibFile()) { @@ -237,12 +238,17 @@ public List getEntries() { return database.getEntries(); } + /** + * @return The path to store the lucene index files. One directory for each library. + */ public Path getFulltextIndexPath() { Path appData = OS.getNativeDesktop().getFulltextIndexBaseDirectory(); Path indexPath; if (getDatabasePath().isPresent()) { - indexPath = appData.resolve(String.valueOf(this.getDatabasePath().get().hashCode())); + Path databaseFileName = getDatabasePath().get().getFileName(); + String fileName = BackupFileUtil.getUniqueFilePrefix(databaseFileName) + "--" + databaseFileName; + indexPath = appData.resolve(fileName); LOGGER.debug("Index path for {} is {}", getDatabasePath().get(), indexPath); return indexPath; } diff --git a/src/main/java/org/jabref/model/database/BibDatabases.java b/src/main/java/org/jabref/model/database/BibDatabases.java index 750454c84d1..6ca03178637 100644 --- a/src/main/java/org/jabref/model/database/BibDatabases.java +++ b/src/main/java/org/jabref/model/database/BibDatabases.java @@ -14,7 +14,7 @@ private BibDatabases() { /** * Receives a Collection of BibEntry instances, iterates through them, and * removes all entries that have no fields set. This is useful for rooting out - * an unsucessful import (wrong format) that returns a number of empty entries. + * an unsuccessful import (wrong format) that returns a number of empty entries. */ public static List purgeEmptyEntries(Collection entries) { return entries.stream() diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 2b7519438eb..8a61da21ca4 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -54,25 +54,23 @@ /** * Represents a Bib(La)TeX entry, which can be BibTeX or BibLaTeX. *

- * Example: + * Example: * - *

{@code
+ * 
{@code
  * Some commment
  * @misc{key,
  *   fieldName = {fieldValue},
- *   otherFieldName = {otherVieldValue}
+ *   otherFieldName = {otherFieldValue}
  * }
- *     }
- * - * Then, + * }
+ *

+ * Then, *

    *
  • "Some comment" is the comment before the entry,
  • *
  • "misc" is the entry type
  • *
  • "key" the citation key
  • *
  • "fieldName" and "otherFieldName" the fields of the BibEntry
  • *
- *

- *

* A BibTeX entry has following properties: *

    *
  • comments before entry
  • @@ -89,7 +87,7 @@ *
*

*

- * In case you search for a builder as described in Item 2 of the book "Effective Java", you won't find one. Please use the methods {@link #withCitationKey(String)} and {@link #withField(Field, String)}. + * In case you search for a builder as described in Item 2 of the book "Effective Java", you won't find one. Please use the methods {@link #withCitationKey(String)} and {@link #withField(Field, String)}. All these methods set {@link #hasChanged()} to false. In case changed, use {@link #withChanged(boolean)}. *

*/ @AllowedToUseLogic("because it needs access to parser and writers") @@ -945,6 +943,7 @@ public BibEntry withField(Field field, String value) { */ public BibEntry withFields(Map content) { this.fields = FXCollections.observableMap(new HashMap<>(content)); + this.setChanged(false); return this; } @@ -969,6 +968,7 @@ public String getUserComments() { public BibEntry withUserComments(String commentsBeforeEntry) { this.commentsBeforeEntry = commentsBeforeEntry; + this.setChanged(false); return this; } @@ -1039,6 +1039,12 @@ public Optional setFiles(List files) { return this.setField(StandardField.FILE, newValue); } + public BibEntry withFiles(List files) { + setFiles(files); + this.setChanged(false); + return this; + } + /** * Gets a list of linked files. * diff --git a/src/main/java/org/jabref/model/entry/LinkedFile.java b/src/main/java/org/jabref/model/entry/LinkedFile.java index e3c296f5c90..0eeb1c60bc0 100644 --- a/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -18,15 +18,20 @@ import javafx.beans.property.StringProperty; import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.logic.util.FileType; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + /** * Represents the link to an external file (e.g. associated PDF file). * This class is {@link Serializable} which is needed for drag and drop in gui */ @AllowedToUseLogic("Uses FileUtil from logic") +@NullMarked public class LinkedFile implements Serializable { private static final String REGEX_URL = "^((?:https?\\:\\/\\/|www\\.)(?:[-a-z0-9]+\\.)*[-a-z0-9]+.*)"; @@ -43,8 +48,12 @@ public LinkedFile(String description, Path link, String fileType) { this(Objects.requireNonNull(description), Objects.requireNonNull(link).toString(), Objects.requireNonNull(fileType)); } + public LinkedFile(String description, String link, FileType fileType) { + this(description, link, fileType.getName()); + } + /** - * Constructor for non-valid paths. We need to parse them, because the GUI needs to render it. + * Constructor can also be used for non-valid paths. We need to parse them, because the GUI needs to render it. */ public LinkedFile(String description, String link, String fileType) { this.description.setValue(Objects.requireNonNull(description)); @@ -80,6 +89,10 @@ public void setFileType(String fileType) { this.fileType.setValue(fileType); } + public void setFileType(FileType fileType) { + this.setFileType(fileType.getName()); + } + public String getDescription() { return description.get(); } @@ -105,7 +118,7 @@ public Observable[] getObservables() { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -128,7 +141,7 @@ private void writeObject(ObjectOutputStream out) throws IOException { } /** - * Reads serialized object from ObjectInputStreamm, automatically called + * Reads serialized object from {@link ObjectInputStream}, automatically called */ private void readObject(ObjectInputStream in) throws IOException { fileType = new SimpleStringProperty(in.readUTF()); diff --git a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java index 618d1771b95..8f4e7aad63c 100644 --- a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java @@ -54,6 +54,10 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } - return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT)) { + return false; + } + + return getFulltextResults(query, bibEntry).numSearchResults() > 0; } } diff --git a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java index 27cfbf65b8b..110d4128bb8 100644 --- a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java @@ -4,12 +4,12 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; -import java.util.stream.Collectors; import org.jabref.architecture.AllowedToUseLogic; import org.jabref.gui.Globals; -import org.jabref.logic.pdf.search.retrieval.PdfSearcher; -import org.jabref.model.database.BibDatabaseContext; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; +import org.jabref.logic.pdf.search.PdfSearcher; import org.jabref.model.entry.BibEntry; import org.jabref.model.pdf.search.PdfSearchResults; import org.jabref.model.pdf.search.SearchResult; @@ -30,16 +30,12 @@ public abstract class FullTextSearchRule implements SearchRule { protected final EnumSet searchFlags; protected String lastQuery; - protected List lastSearchResults; - - private final BibDatabaseContext databaseContext; + protected List lastPdfSearchResults; public FullTextSearchRule(EnumSet searchFlags) { this.searchFlags = searchFlags; this.lastQuery = ""; - lastSearchResults = Collections.emptyList(); - - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); + lastPdfSearchResults = Collections.emptyList(); } public EnumSet getSearchFlags() { @@ -48,24 +44,37 @@ public EnumSet getSearchFlags() { @Override public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT)) { + LOGGER.debug("Fulltext search results called even though fulltext search flag is missing."); return new PdfSearchResults(); } - if (!query.equals(this.lastQuery)) { + if (query.equals(this.lastQuery)) { + LOGGER.trace("Reusing fulltext search results (query={}, lastQuery={}).", query, this.lastQuery); + } else { + LOGGER.trace("Performing full query {}.", query); + PdfIndexer pdfIndexer; + try { + pdfIndexer = PdfIndexerManager.getIndexer(Globals.stateManager.getActiveDatabase().get(), Globals.prefs.getFilePreferences()); + } catch (IOException e) { + LOGGER.error("Could not access full text index.", e); + return new PdfSearchResults(); + } this.lastQuery = query; - lastSearchResults = Collections.emptyList(); + lastPdfSearchResults = Collections.emptyList(); try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearcher searcher = PdfSearcher.of(pdfIndexer); PdfSearchResults results = searcher.search(query, 5); - lastSearchResults = results.getSortedByScore(); + lastPdfSearchResults = results.getSortedByScore(); } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); + LOGGER.error("Could not retrieve search results.", e); + return new PdfSearchResults(); } } - return new PdfSearchResults(lastSearchResults.stream() - .filter(searchResult -> searchResult.isResultFor(bibEntry)) - .collect(Collectors.toList())); + // We found a number of PDF files, now we need to relate it to the current BibEntry + return new PdfSearchResults(lastPdfSearchResults.stream() + .filter(searchResult -> searchResult.isResultFor(bibEntry)) + .toList()); } } diff --git a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java index fd281a11a0f..768c88f0303 100644 --- a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java @@ -1,6 +1,5 @@ package org.jabref.model.search.rules; -import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -13,14 +12,10 @@ import java.util.stream.Collectors; import org.jabref.architecture.AllowedToUseLogic; -import org.jabref.gui.Globals; -import org.jabref.logic.pdf.search.retrieval.PdfSearcher; -import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.Keyword; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.InternalField; -import org.jabref.model.pdf.search.PdfSearchResults; import org.jabref.model.pdf.search.SearchResult; import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; @@ -45,7 +40,7 @@ * This class implements the "Advanced Search Mode" described in the help */ @AllowedToUseLogic("Because access to the lucene index is needed") -public class GrammarBasedSearchRule implements SearchRule { +public class GrammarBasedSearchRule extends FullTextSearchRule { private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); @@ -55,8 +50,6 @@ public class GrammarBasedSearchRule implements SearchRule { private String query; private List searchResults = new ArrayList<>(); - private final BibDatabaseContext databaseContext; - public static class ThrowingErrorListener extends BaseErrorListener { public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); @@ -70,8 +63,8 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, } public GrammarBasedSearchRule(EnumSet searchFlags) throws RecognitionException { + super(searchFlags); this.searchFlags = searchFlags; - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } public static boolean isValid(EnumSet searchFlags, String query) { @@ -97,20 +90,9 @@ private void init(String query) throws ParseCancellationException { SearchParser parser = new SearchParser(new CommonTokenStream(lexer)); parser.removeErrorListeners(); // no infos on file system parser.addErrorListener(ThrowingErrorListener.INSTANCE); - parser.setErrorHandler(new BailErrorStrategy()); // ParseCancelationException on parse errors + parser.setErrorHandler(new BailErrorStrategy()); // ParseCancellationException on parse errors tree = parser.start(); this.query = query; - - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || (databaseContext == null)) { - return; - } - try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); - PdfSearchResults results = searcher.search(query, 5); - searchResults = results.getSortedByScore(); - } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); - } } @Override @@ -118,16 +100,11 @@ public boolean applyRule(String query, BibEntry bibEntry) { try { return new BibtexSearchVisitor(searchFlags, bibEntry).visit(tree); } catch (Exception e) { - LOGGER.debug("Search failed", e); - return getFulltextResults(query, bibEntry).numSearchResults() > 0; + LOGGER.info("Search failed", e); + return false; } } - @Override - public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - return new PdfSearchResults(searchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); - } - @Override public boolean validateSearchStrings(String query) { try { diff --git a/src/test/java/org/jabref/architecture/TestArchitectureTest.java b/src/test/java/org/jabref/architecture/TestArchitectureTest.java index 81f73e5a117..9df4d5b2652 100644 --- a/src/test/java/org/jabref/architecture/TestArchitectureTest.java +++ b/src/test/java/org/jabref/architecture/TestArchitectureTest.java @@ -25,6 +25,7 @@ public void testsAreIndependent(JavaClasses classes) { .and().doNotHaveSimpleName("PreferencesMigrationsTest") .and().doNotHaveSimpleName("SaveDatabaseActionTest") .and().doNotHaveSimpleName("UpdateTimestampListenerTest") + .and().doNotHaveSimpleName("DatabaseSearcherWithBibFilesTest") .and().doNotHaveFullyQualifiedName("org.jabref.benchmarks.Benchmarks") .and().doNotHaveFullyQualifiedName("org.jabref.testutils.interactive.styletester.StyleTesterMain") .should().dependOnClassesThat().haveFullyQualifiedName(CLASS_ORG_JABREF_GLOBALS) diff --git a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java index 06a4cfc361f..be3f9274d4a 100644 --- a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java @@ -12,7 +12,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.logic.preferences.OwnerPreferences; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; diff --git a/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java b/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java index b6737e8c924..90676249146 100644 --- a/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java +++ b/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.jabref.logic.formatter.IdentityFormatter; import org.jabref.logic.formatter.bibtexfields.EscapeAmpersandsFormatter; @@ -18,11 +19,15 @@ import org.jabref.logic.layout.format.ReplaceUnicodeLigaturesFormatter; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.InternalField; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -72,7 +77,7 @@ public void checkLowerCaseSaveAction() { FieldFormatterCleanups actions = new FieldFormatterCleanups(true, FieldFormatterCleanups.parse("title[lower_case]")); FieldFormatterCleanup lowerCaseTitle = new FieldFormatterCleanup(StandardField.TITLE, new LowerCaseFormatter()); - assertEquals(Collections.singletonList(lowerCaseTitle), actions.getConfiguredActions()); + assertEquals(List.of(lowerCaseTitle), actions.getConfiguredActions()); actions.applySaveActions(entry); @@ -309,6 +314,24 @@ void parserTest() { fieldFormatterCleanups); } + @Test + void identityCanBeParsed() { + List fieldFormatterCleanups = FieldFormatterCleanups.parse(""" + all-text-fields[identity] + date[normalize_date] + month[normalize_month] + pages[normalize_page_numbers] + """); + assertEquals( + List.of( + new FieldFormatterCleanup(InternalField.INTERNAL_ALL_TEXT_FIELDS_FIELD, new IdentityFormatter()), + new FieldFormatterCleanup(StandardField.DATE, new NormalizeDateFormatter()), + new FieldFormatterCleanup(StandardField.MONTH, new NormalizeMonthFormatter()), + new FieldFormatterCleanup(StandardField.PAGES, new NormalizePagesFormatter()) + ), + fieldFormatterCleanups); + } + @Test void getMetaDataStringWorks() { assertEquals(""" @@ -329,8 +352,16 @@ void parsingOfDefaultSaveActions() { """)); } - @Test - void formatterFromString() { - assertEquals(new ReplaceUnicodeLigaturesFormatter(), FieldFormatterCleanups.getFormatterFromString("replace_unicode_ligatures")); + public static Stream formatterFromString() { + return Stream.of( + Arguments.of(new ReplaceUnicodeLigaturesFormatter(), "replace_unicode_ligatures"), + Arguments.of(new LowerCaseFormatter(), "lower_case") + ); + } + + @ParameterizedTest + @MethodSource + void formatterFromString(Formatter expected, String input) { + assertEquals(expected, FieldFormatterCleanups.getFormatterFromString(input)); } } diff --git a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java index 251b3d10279..d043c51fe79 100644 --- a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java +++ b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java @@ -1,13 +1,22 @@ package org.jabref.logic.importer.util; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.jabref.logic.cleanup.FieldFormatterCleanup; +import org.jabref.logic.cleanup.FieldFormatterCleanups; import org.jabref.logic.exporter.MetaDataSerializerTest; +import org.jabref.logic.formatter.casechanger.LowerCaseFormatter; import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; import org.jabref.model.entry.types.UnknownEntryType; +import org.jabref.model.metadata.MetaData; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; @@ -56,4 +65,16 @@ public static Stream parseCustomizedEntryType() { void parseCustomizedEntryType(BibEntryTypeBuilder expected, String source) { assertEquals(Optional.of(expected.build()), MetaDataParser.parseCustomEntryType(source)); } + + @Test + public void saveActions() throws Exception { + Map data = Map.of("saveActions", "enabled;title[lower_case]"); + MetaDataParser metaDataParser = new MetaDataParser(new DummyFileUpdateMonitor()); + MetaData parsed = metaDataParser.parse(new MetaData(), data, ','); + + MetaData expected = new MetaData(); + FieldFormatterCleanups fieldFormatterCleanups = new FieldFormatterCleanups(true, List.of(new FieldFormatterCleanup(StandardField.TITLE, new LowerCaseFormatter()))); + expected.setSaveActions(fieldFormatterCleanups); + assertEquals(expected, parsed); + } } diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java b/src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java similarity index 98% rename from src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java rename to src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java index 982504c9938..135944c722c 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.nio.file.Path; import java.util.Collections; diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java b/src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java similarity index 74% rename from src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java rename to src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java index 629bb293778..187f01ce656 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Path; @@ -53,8 +53,7 @@ public void exampleThesisIndex() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -63,18 +62,17 @@ public void exampleThesisIndex() throws IOException { } @Test - public void dontIndexNonPdf() throws IOException { + public void doNotIndexNonPdf() throws IOException { // given - BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); - entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.AUX.getName()))); + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis) + .withFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.aux", StandardFileType.AUX.getName()))); database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + try (IndexReader reader = DirectoryReader.open(indexer.indexWriter)) { assertEquals(0, reader.numDocs()); } } @@ -87,11 +85,10 @@ public void dontIndexOnlineLinks() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + try (IndexReader reader = DirectoryReader.open(indexer.indexWriter)) { assertEquals(0, reader.numDocs()); } } @@ -105,8 +102,7 @@ public void exampleThesisIndexWithKey() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -123,8 +119,7 @@ public void metaDataIndex() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -132,30 +127,6 @@ public void metaDataIndex() throws IOException { } } - @Test - public void testFlushIndex() throws IOException { - // given - BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); - entry.setCitationKey("Example2017"); - entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); - database.insertEntry(entry); - - indexer.createIndex(); - indexer.addToIndex(context); - // index actually exists - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, reader.numDocs()); - } - - // when - indexer.flushIndex(); - - // then - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(0, reader.numDocs()); - } - } - @Test public void exampleThesisIndexAppendMetaData() throws IOException { // given @@ -163,8 +134,8 @@ public void exampleThesisIndexAppendMetaData() throws IOException { exampleThesis.setCitationKey("ExampleThesis2017"); exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(exampleThesis); - indexer.createIndex(); - indexer.addToIndex(context); + + indexer.rebuildIndex(); // index with first entry try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -176,7 +147,7 @@ public void exampleThesisIndexAppendMetaData() throws IOException { metadata.setFiles(Collections.singletonList(new LinkedFile("Metadata file", "metaData.pdf", StandardFileType.PDF.getName()))); // when - indexer.addToIndex(metadata, null); + indexer.addToIndex(metadata); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -184,4 +155,3 @@ public void exampleThesisIndexAppendMetaData() throws IOException { } } } - diff --git a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java b/src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java similarity index 76% rename from src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java rename to src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java index e005c271ff1..b06bda0d328 100644 --- a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java @@ -1,10 +1,10 @@ -package org.jabref.logic.pdf.search.retrieval; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; +import java.util.List; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -12,6 +12,7 @@ import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; import org.jabref.preferences.FilePreferences; import org.apache.lucene.queryparser.classic.ParseException; @@ -32,44 +33,48 @@ public class PdfSearcherTest { @BeforeEach public void setUp(@TempDir Path indexDir) throws IOException { FilePreferences filePreferences = mock(FilePreferences.class); - // given + BibDatabase database = new BibDatabase(); + BibDatabaseContext context = mock(BibDatabaseContext.class); when(context.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); when(context.getFulltextIndexPath()).thenReturn(indexDir); when(context.getDatabase()).thenReturn(database); when(context.getEntries()).thenReturn(database.getEntries()); - BibEntry examplePdf = new BibEntry(StandardEntryType.Article); - examplePdf.setFiles(Collections.singletonList(new LinkedFile("Example Entry", "example.pdf", StandardFileType.PDF.getName()))); + + BibEntry examplePdf = new BibEntry(StandardEntryType.Article) + .withFiles(Collections.singletonList(new LinkedFile("Example Entry", "example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(examplePdf); - BibEntry metaDataEntry = new BibEntry(StandardEntryType.Article); - metaDataEntry.setFiles(Collections.singletonList(new LinkedFile("Metadata Entry", "metaData.pdf", StandardFileType.PDF.getName()))); - metaDataEntry.setCitationKey("MetaData2017"); + BibEntry metaDataEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("MetaData2017") + .withFiles(Collections.singletonList(new LinkedFile("Metadata Entry", "metaData.pdf", StandardFileType.PDF.getName()))); database.insertEntry(metaDataEntry); - BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis); - exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); - exampleThesis.setCitationKey("ExampleThesis"); + BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis) + .withCitationKey("ExampleThesis") + .withFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(exampleThesis); PdfIndexer indexer = PdfIndexer.of(context, filePreferences); - search = PdfSearcher.of(context); + search = PdfSearcher.of(indexer); - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); } @Test public void searchForTest() throws IOException, ParseException { PdfSearchResults result = search.search("test", 10); - assertEquals(8, result.numSearchResults()); + assertEquals(10, result.numSearchResults()); } @Test public void searchForUniversity() throws IOException, ParseException { PdfSearchResults result = search.search("University", 10); - assertEquals(1, result.numSearchResults()); + assertEquals(2, result.numSearchResults()); + List searchResults = result.getSearchResults(); + assertEquals(0, searchResults.get(0).getPageNumber()); + assertEquals(9, searchResults.get(1).getPageNumber()); } @Test diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java index f276f9ddfaf..fa38b68cc03 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -66,9 +66,7 @@ public void testCorrectMatchFromDatabaseWithArticleTypeEntry() { @Test public void testNoMatchesFromEmptyDatabaseWithInvalidQuery() { SearchQuery query = new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); - assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); } diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java new file mode 100644 index 00000000000..84a54d6b3c1 --- /dev/null +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java @@ -0,0 +1,172 @@ +package org.jabref.logic.search; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jabref.architecture.AllowedToUseSwing; +import org.jabref.gui.Globals; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@AllowedToUseSwing("Makes use of Globals because of FullTextSearchRule") +public class DatabaseSearcherWithBibFilesTest { + + private static BibEntry titleSentenceCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-sentence-cased") + .withField(StandardField.TITLE, "Title Sentence Cased"); + private static BibEntry titleMixedCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-mixed-cased") + .withField(StandardField.TITLE, "TiTle MiXed CaSed"); + private static BibEntry titleUpperCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-upper-cased") + .withField(StandardField.TITLE, "TITLE UPPER CASED"); + + private static BibEntry mininimalSentenceCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-sentence-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-sentence-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalAllUpperCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-all-upper-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-all-upper-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalMixedCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-mixed-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-mixed-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteSentenceCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-sentence-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-sentence-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteAllUpperCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-all-upper-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-all-upper-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteMixedCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-mixed-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-mixed-case.pdf", StandardFileType.PDF.getName()))); + + FilePreferences filePreferences = mock(FilePreferences.class); + + @TempDir + private Path indexDir; + private PdfIndexer pdfIndexer; + + private BibDatabase initializeDatabaseFromPath(String testFile) throws Exception { + return initializeDatabaseFromPath(Path.of(Objects.requireNonNull(DatabaseSearcherWithBibFilesTest.class.getResource(testFile)).toURI())); + } + + private BibDatabase initializeDatabaseFromPath(Path testFile) throws Exception { + ParserResult result = new BibtexImporter(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()).importDatabase(testFile); + BibDatabase database = result.getDatabase(); + + BibDatabaseContext context = mock(BibDatabaseContext.class); + when(context.getFileDirectories(Mockito.any())).thenReturn(List.of(testFile.getParent())); + when(context.getFulltextIndexPath()).thenReturn(indexDir); + when(context.getDatabase()).thenReturn(database); + when(context.getEntries()).thenReturn(database.getEntries()); + + // Required because of {@Link org.jabref.model.search.rules.FullTextSearchRule.FullTextSearchRule} + Globals.stateManager.setActiveDatabase(context); + PreferencesService preferencesService = mock(PreferencesService.class); + when(preferencesService.getFilePreferences()).thenReturn(filePreferences); + Globals.prefs = preferencesService; + + pdfIndexer = PdfIndexerManager.getIndexer(context, filePreferences); + // Alternative - For debugging with Luke (part of the Apache Lucene distribution) + // pdfIndexer = PdfIndexer.of(context, Path.of("C:\\temp\\index"), filePreferences); + + pdfIndexer.rebuildIndex(); + return database; + } + + @AfterEach + public void tearDown() throws Exception { + pdfIndexer.close(); + } + + private static Stream searchLibrary() { + return Stream.of( + // empty library + Arguments.of(List.of(), "empty.bib", "Test", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + // test-library-title-casing + + Arguments.of(List.of(), "test-library-title-casing.bib", "NotExisting", EnumSet.noneOf(SearchRules.SearchFlags.class)), + Arguments.of(List.of(titleSentenceCased, titleMixedCased, titleUpperCased), "test-library-title-casing.bib", "Title", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=NotExisting", EnumSet.noneOf(SearchRules.SearchFlags.class)), + Arguments.of(List.of(titleSentenceCased, titleMixedCased, titleUpperCased), "test-library-title-casing.bib", "title=Title", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=TiTLE", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleSentenceCased), "test-library-title-casing.bib", "title=Title", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "TiTLE", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleMixedCased), "test-library-title-casing.bib", "TiTle", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=NotExisting", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleMixedCased), "test-library-title-casing.bib", "title=TiTle", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "[Y]", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), + Arguments.of(List.of(titleUpperCased), "test-library-title-casing.bib", "[U]", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), + + // Word boundaries + Arguments.of(List.of(), "test-library-title-casing.bib", "\\bTit\\b", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION, SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleSentenceCased), "test-library-title-casing.bib", "\\bTitle\\b", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION, SearchRules.SearchFlags.CASE_SENSITIVE)), + + // test-library-with-attached-files + + Arguments.of(List.of(), "test-library-with-attached-files.bib", "This is a test.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(mininimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "This is a short sentence, comma included.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + Arguments.of(List.of(mininimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "comma", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + // TODO: PDF search does not support case sensitive search (yet) + // Arguments.of(List.of(minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "THIS", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalAllUpperCase), "test-library-with-attached-files.bib", "THIS is a short sentence, comma included.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "comma", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalNoteAllUpperCase), "test-library-with-attached-files.bib", "THIS IS A SHORT SENTENCE, COMMA INCLUDED.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-with-attached-files.bib", "NotExisting", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + + Arguments.of(List.of(minimalNoteSentenceCase, minimalNoteAllUpperCase, minimalNoteMixedCase), "test-library-with-attached-files.bib", "world", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + Arguments.of(List.of(minimalNoteSentenceCase, minimalNoteAllUpperCase, minimalNoteMixedCase), "test-library-with-attached-files.bib", "Hello World", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + // TODO: PDF search does not support case sensitive search (yet) + // Arguments.of(List.of(minimalNoteAllUpperCase), "test-library-with-attached-files.bib", "HELLO WORLD", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(), "test-library-with-attached-files.bib", "NotExisting", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)) + ); + } + + @ParameterizedTest(name = "{index} => query={2}, searchFlags={3}, testFile={1}, expected={0}") + @MethodSource + public void searchLibrary(List expected, String testFile, String query, EnumSet searchFlags) throws Exception { + BibDatabase database = initializeDatabaseFromPath(testFile); + List matches = new DatabaseSearcher(new SearchQuery(query, searchFlags), database).getMatches(); + assertEquals(expected, matches); + } +} + + diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index 4bf01fadb73..399c89089e6 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -20,8 +20,8 @@ public class SearchQueryTest { @Test public void testToString() { - assertEquals("\"asdf\" (case sensitive, regular expression)", new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).toString()); - assertEquals("\"asdf\" (case insensitive, plain text)", new SearchQuery("asdf", EnumSet.noneOf(SearchFlags.class)).toString()); + assertEquals("\"asdf\" (case sensitive, regular expression) [CASE_SENSITIVE, REGULAR_EXPRESSION]", new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).toString()); + assertEquals("\"asdf\" (case insensitive, plain text) []", new SearchQuery("asdf", EnumSet.noneOf(SearchFlags.class)).toString()); } @Test diff --git a/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java b/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java index 55e4fd2a744..dfb069680d0 100644 --- a/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java +++ b/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -150,9 +151,11 @@ void testGetFullTextIndexPathWhenPathIsNotNull() { BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(); bibDatabaseContext.setDatabasePath(existingPath); - Path expectedPath = OS.getNativeDesktop().getFulltextIndexBaseDirectory().resolve(existingPath.hashCode() + ""); Path actualPath = bibDatabaseContext.getFulltextIndexPath(); + assertNotNull(actualPath); - assertEquals(expectedPath, actualPath); + String fulltextIndexBaseDirectory = OS.getNativeDesktop().getFulltextIndexBaseDirectory().toString(); + String actualPathStart = actualPath.toString().substring(0, fulltextIndexBaseDirectory.length()); + assertEquals(fulltextIndexBaseDirectory, actualPathStart); } } diff --git a/src/test/java/org/jabref/model/groups/SearchGroupTest.java b/src/test/java/org/jabref/model/groups/SearchGroupTest.java index 5bdec41d87e..75829b5a9b5 100644 --- a/src/test/java/org/jabref/model/groups/SearchGroupTest.java +++ b/src/test/java/org/jabref/model/groups/SearchGroupTest.java @@ -1,9 +1,11 @@ package org.jabref.model.groups; import java.util.EnumSet; +import java.util.List; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.Test; @@ -13,6 +15,32 @@ public class SearchGroupTest { + private static BibEntry entry1D = new BibEntry(StandardEntryType.Misc) + .withCitationKey("entry1") + .withField(StandardField.AUTHOR, "Test") + .withField(StandardField.TITLE, "Case") + .withField(StandardField.GROUPS, "A"); + + private static BibEntry entry2D = new BibEntry(StandardEntryType.Misc) + .withCitationKey("entry2") + .withField(StandardField.AUTHOR, "TEST") + .withField(StandardField.TITLE, "CASE") + .withField(StandardField.GROUPS, "A"); + + @Test + public void containsFindsWords() { + SearchGroup groupPositive = new SearchGroup("A", GroupHierarchyType.INDEPENDENT, "Test", EnumSet.noneOf(SearchRules.SearchFlags.class)); + List positiveResult = List.of(entry1D, entry2D); + assertTrue(groupPositive.containsAll(positiveResult)); + } + + @Test + public void containsDoesNotFindWords() { + SearchGroup groupNegative = new SearchGroup("A", GroupHierarchyType.INDEPENDENT, "Unknown", EnumSet.noneOf(SearchRules.SearchFlags.class)); + List positiveResult = List.of(entry1D, entry2D); + assertFalse(groupNegative.containsAny(positiveResult)); + } + @Test public void containsFindsWordWithRegularExpression() { SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); diff --git a/src/test/search/resources/.gitignore b/src/test/resources/org/jabref/logic/search/.gitignore similarity index 83% rename from src/test/search/resources/.gitignore rename to src/test/resources/org/jabref/logic/search/.gitignore index 30aaa5e7abe..a5db2fdd1bd 100644 --- a/src/test/search/resources/.gitignore +++ b/src/test/resources/org/jabref/logic/search/.gitignore @@ -1,6 +1,8 @@ # LaTeX temp files *.aux +*.out +*.log *.synctex *.synctex.gz diff --git a/src/test/resources/org/jabref/logic/search/README.md b/src/test/resources/org/jabref/logic/search/README.md new file mode 100644 index 00000000000..f607cfa269f --- /dev/null +++ b/src/test/resources/org/jabref/logic/search/README.md @@ -0,0 +1,11 @@ +# Libraries and PDFs for testing search functionality + +This folder contains manual use-cases for testing the search functionality. + +It is used at the test code `org.jabref.logic.search.DatabaseSearcherWithBibFilesTest`. + +The `.pdf` files are generated from the `.tex` files using `pdflatex`. + +Compile all files in cmd.exe: + + for %f in (*.tex) do pdflatex "%f" diff --git a/src/test/search/resources/empty.bib b/src/test/resources/org/jabref/logic/search/empty.bib similarity index 100% rename from src/test/search/resources/empty.bib rename to src/test/resources/org/jabref/logic/search/empty.bib diff --git a/src/test/search/resources/minimal-note1.pdf b/src/test/resources/org/jabref/logic/search/minimal-all-upper-case.pdf similarity index 89% rename from src/test/search/resources/minimal-note1.pdf rename to src/test/resources/org/jabref/logic/search/minimal-all-upper-case.pdf index d6497ba5bbb93c796917d5fe9639d9d2e54d3581..764b3807a3f79c3a95c5816dfc8cd86205b638ac 100644 GIT binary patch delta 836 zcmaiyOK1~89L7oGp^Wc{k7BW7N=Qm+c4i;h-53H*(uI@;LlZ>}QPO0qDRu`ot41rt zAfzqWgUm$`kzTwg)KWnNuLTbt0##`t7Y`y;!Gjb;q;>K@X+)gU%*XHl@!xO1&0@ve z=ZZ(P0qm84tNLbn6$p2OvC`xFTgRliXJd({pQA5lzixFp*Yn@+m0!&G&rep$l}o*^ z4zFGuzI^%BsS5%Q9b)kUz;whR2FLwvv2S8 zu0OBcy6q-iBQvw+rwcz9k_R{J*B6|5pc}b@W$Ky9`i+cDP+)VG1c=}$;70WVxngGZ z0>n)rrswjR5GleGVuGYfUS9S=MZ{i3O#?2TF?GX&QUeYKTaKVU%$z|W062{>AP|6@ z#w$>)gOGt-+$7nAZow4fSS0kE{Q$k$(Mhk`KiK4@^p1~SIwc>XDWSGsZZlVxhDB_r zefC{+S`OP3`daoleGQ09l2b-LOL96?+omMirKt3X;-H(#UdLYxV|rR0NQ7A|7BQhG zsh{ZsT;rK1MkHhD&=c|Vh!+t?GEzlRkoysKBjjdWjO6}_(4GE4n~%o)(8g24-`yh! zJVf;a4G0<+gm|7M*3@X(f3$=J`H#|~u%O5+z9#j1Qe-~unonSnZR0TS6WJ%XX)0oW zeY+;80^7vVE-IM5Kha&{SwRS~Ag*_TwIynd6Np<)iAG>5ps8Azd1xUl3sOYFAyFC% mg?TX|<3YB*6omgr()E56tc+2H1G=-SnlNj delta 1684 zcmah~U1%d!6sGNyD-^0J3kT%NhetsQ&ZBE-5Lv~?Zl=gGbJ-& zvn!alg2IAfeDgu}#Rpk%mlb?bQBYs~X^|BLA4L#RbbZ)|UC*7EHc2VYOYS}QoO{0S zobTN1H1D_PrWc*uEK`Lt{mtH&cN7K`{IxA5l>)K^i#9MhiXhXIs9i)pPY2b83xR{({Su57zNVrPXcwbsITUvk?anTN8x zi>(5gM=)W{_nb=TLc)2LQ?5}#ZQh}};9`byN|QhqhR%kL=F`Ab3Zw)Hbi_nk@#2L_ z1Jlp@jfTXI3opb)0Ey?a`F!Ces?&>wV&1`!!nW%{wNekzKL)1oT-^CS|N zx;SVq({FEbHW9h($>;-NOTS-5;wdIokNPS3%U*5W-*AEH(h|LFoQfPH(UJP{_1UGc(hQRzOHhQwcI6dUWl4~w#BG2rKv|rVCdssjI?UG^H7UHo=t0N9!m|CA zOr;d!xB&*Tp3(;xnNh=l3w>etz{QcsXAuD{ARV$CxP?|&uX%2u#9~Ru(4fbA;5I9I z*O=|3Tj8ePOe9IE7S>(-m6Aj@xQfM&XZcFWy#$wQ;U;Y2R5Y7zHA$X#uUz(<8$lwe z96c3%<)5YWEczk7(ZP9$_kE(%W_nCS6%!_Hh3bVSt8dL^W%wW8=f&`}j`AidM)4uj&J{qn)oQ!xH}ay6?mUsi0X5%zS%aHr`(l>u`zfh0BUwcE1l3Y^LpgwPx!ko!M4D zvkE1jzu$E`W~$=OXHuURF0}B!Y^P&dSEs2S;=ix)8{gcE7kjZz|_py%+=Y*z)r!2kdnzCtz-ba C;8J=3 delta 240 zcmeAz>n)oQ!?Jky&+Lf_QbChi9r@?ke2~8I%}T-1g0=TagRqTC|I>9=$vrx!mxb+% ziaGXk_dBz2SI2cbkJV&ooV&b*-@A8y{3*}X>h~GuUY#aj^}P9=v-QEXLgHfev)fdK zCZ3tWXtFt#ae}#_v4N?9rICS&iLr^cfswj_fx0G_zHfetOJYf?f`*Hgk%1AalF4e8 zj*O<8Gc4U0olPu_ot#ZfT-=;ZoGe{jOf8*UOkJGZj2ul&K_ZrR3O0n4O#Wyk0{}kv BQ3?P6 diff --git a/src/test/search/resources/minimal2.tex b/src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex similarity index 80% rename from src/test/search/resources/minimal2.tex rename to src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex index 42b3e319f8d..9c71a665bac 100644 --- a/src/test/search/resources/minimal2.tex +++ b/src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex @@ -1,9 +1,5 @@ \documentclass{article} -\usepackage[utf8]{inputenc} \usepackage[english]{babel} - \begin{document} - ThiS is a short sentence, comma included. - \end{document} diff --git a/src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf b/src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6139da156a2dd1ae53ddaa711683984c93bb8fb6 GIT binary patch literal 15771 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 JEGvTa{{d$*9kBoa literal 0 HcmV?d00001 diff --git a/src/test/search/resources/minimal-note.tex b/src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex similarity index 63% rename from src/test/search/resources/minimal-note.tex rename to src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex index c47b1aa8b5d..9042f3b87ef 100644 --- a/src/test/search/resources/minimal-note.tex +++ b/src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex @@ -1,13 +1,7 @@ \documentclass{article} -\usepackage[utf8]{inputenc} \usepackage[english]{babel} - \usepackage{pdfcomment} - \begin{document} - -This is a short sentence, comma included. - +Demonstration of different cases of a comment. \pdfcomment{Hello World} - \end{document} diff --git a/src/test/search/resources/minimal.pdf b/src/test/resources/org/jabref/logic/search/minimal-sentence-case.pdf similarity index 96% rename from src/test/search/resources/minimal.pdf rename to src/test/resources/org/jabref/logic/search/minimal-sentence-case.pdf index 3658e1118b01792c1f7d6e23b4f0641a5bd59ceb..0c9533132a8a728af2e1bcb7717425373bc545fa 100644 GIT binary patch delta 466 zcmbPKGPz`e3bVO_!sJ|L^?KdjgItFU1YF+#?6Or$2rr*pa(VRv_VYVC**%qXZ;0gl zt)KYC#5kYrnatIXrx@8}s@^H?Ylv}BWV7THs_u|{XVrH(%PMIME~SgoXL#+5qp%rr)e z&B=_@%?wQp3=PZ;EKN*I473dl)eQ{PHM#VC^HW?BOHvgyT&#=?j8K(K*0ykDG@G1j zVJ~W=U;qLNc?w)$hJk^Rk>TWn7V4UoXflQ-h9<^nVg?2#1_qM_ErkP3&}0maEDa6O z#f%NjF~m%afVQK$!O+CW)L?R{rCq#{k(-5+vze2*nSrITqq&p0xs!>rrIWFtnWd47 nfq}W5f(=0>u?lu}T*W1cMI{wQscBq>CMMWEZ+U|<~OYu2S49C(yTI3EQf2` zzWp9mvu5VAJ=1wrp~~MZtK>RCe8GENWybk8lvpS5l^4V2?a|&grpWk*n zhXFW;vZ0-~N(%KMq_-M=#>Tds(i=U$c53#XLZ#nuPwm+CF5Tk%ZQh$}U3;<>4e zCYw_jr<)lX8<-kc8X1_Fm>6pt7^xc=sB3cR`{t*(B$lKqXt-Dz85p4|nXF^s$Y?q_ z&%$2RP{9BM6!H|fzzhQeBO`;!hb+|VEzo2PO$<$p(8LT3j4drO#4HSr(ZmdmEDa6N z#f%NjF~m#^Vdn9{odmSj#By?(rCq#>p^Jr~i;KCVqoKK}n}xBXk&A()n~ABLnWLex ng{irnf(=0>u?lu}T*W1cMI{wQscBq>CMITEvaCe76g1fs12?Ptl-3d;R-~@MfcXx+xNX~ut zoO{>%-hUP|v%9*gyQ;dY_pHVKk;@5-(gEpNkjaO~JExI>07ihd!AE3nZUBQKfI-Y! z$r`{Q4A5kP$Q1w#qGk@-0A5~Xu$9rX7R&!=BQr>WtxOzD0YD~Jz@HZ~MBBmvYzMKj z(02d}gAJ{Xz{n6|dj~tPz9q8DEM3G;`1?Xe-x3nIkhP`dv+Mtkok|>RVPUPs$jqo}ZD(NwF_EHb*!CH+0u9|`;Kl>Qmz-`Xs{H9#CdNGKqV=dnTT?2s^j>uLY13L%^J-)sEmI0HiV zFB9{>4mT!9T#yR;R})gT|GESKfgEh?zt6q@``C-9g_F~otD#42C54>=ptUY6ARVH& z=J?8_6PR(c!}>MAA{`zQ&o3;nu#^4Zu9>gAs(aGC&~cpDy+9)|^fFYT`-rTJW^9WR z@iK-39>+Jm`!X^=4xbJUM1an) zdcyIO>BdK{#?}Kn+S=*^s~wGe+0{aaLC{+Ka^^mq!q5`I8VOr(H z#`$*LG3=neHHR-mTzrptfDQWL!}ACq4=4-VAdyyMpDF+7D{v{pUw7r5)EFZ76s-=O z0cFL?%Btff12s$x<-k&F{P38rfQr!3ZLxS1ahmdAn`l-qqUR02AaQ1{K!jy~RW zC0N5S>qd9Y)pl}Y2_i2MG-vKj!U1AtQMAK?IIt|Wk70D)J=KrXOaw1q=Of#~KG>E( zebtTJ^xn}?>AN56n^?IP?BP>xNf@{}idE3F59uVK{na|~g3-~2(NV~gV;!#p?O4xy zMg%2&FO3Yotip$M7!R*S(mfSQGUUZB31x2=oZiH}&^hJm7F^l4NB?W+;>{-kErxgo z0?WJ4kDuMt=^o)`po?3)Tk#O~yalYFXt#BvFj_vIFt2r6(wx@3;bus?!eHW*cce?OTeAhz_ zsE7`5-?HfY1^eM%@T$O7%`1g8JL)gK!AZ?r9QlxGa;aux>R9jY|4LRjb+>lYhMbsh zIJB;jT;xjABKm%0t*jiLI-gpljuW zRY*J(5lkUs+`?<$`SsD0CGf#>KT(VK8|K6rRWfL5t({Y>;~@M_B9tQ7kP8!M1VK>Y z$I^5=HZw8S9nvYRJuzHe-_Nts2*JAbrIAFeghZhFC5J+P3dE zP;i%L#6xe39>dv1LPu_r?`>-dx@?Na2(dgCEuIrRwfRHT2D$I6QQ?5M+ucP@MCpX| z6l0n8+)FR~%+jNRm+Cr*fOp2|1ak{;ojgC2(- zxqF9<`LKot?LvwX2%bQ)5wchIo zI75vz@z6RV#lH@j9$rW5zMsuKJo~vIPp&S?eXhKCF)4-jX{NzY8p#BC?p7R?NP4BA zB>J)u0WbG$-PL$i9f|}GGJ>E$8XoG_(v}mZ@^#3H_k;FPyZl6Ms53;cbcz+ur-y4S##iTv=-2y1P;rL*pp20Kh@Et zStppcIp~wh#*?BIY0$5-gptKjkNs?^&U(~$33V1PNw)a$HcPC+Cs}ZIT)9(Abbf1Q z`uua!p_zg6dNf)aI1*mD=tpxPOjsb=?1f&zJm;(4iTCY30 z08;`b86JWJ86cZ)VK=mpTtV6++TN5(DI1T-S8#eMNyQDX3Kdg-{4gq=mAA4{TODU4 zO`Z~k2vj9MIWFDg?bwfN0@rXm+CxD)UCbM9%S(=;7#DW6C4i^y%NP98K#hG+zlA*o}NMGdq>HK z!a_2gIyt^Hm{*FfmR4d6TO3Dx4Vao#QOXWMf0-Dfb4o$Dn)L?51hCyzt9@Emh-_Ov z6s3bK)MaI)j|&`zbNxabGxBbxJ>ysm~R|zi)i2pphx+BRO_+9uNwhnG_FWh+VgiYuYQ1Llxt6KQ6}n zHGn_w0IkE}QA=_y+Vg?T0}U@Bm!lxBrJ|Vs+TN$hKa}5CuC<+6?bJU&%8i!hO~D-2 zLefjPSq5!F8VkkQP%yV$p^_5o6@oK4T7Nn0-t9p^@K{TQg))l%oy#p#zEZYsWRuQp(c+cS#ai=3p2>J*PG`7 zLWWZW@Fprt89B_Eftr|x3(beEA>V@OOUfJQl-}U>!hb=d=Ew@8QLQvIsN*CoUBC0V zeDw)82CQjK8mu$7KU=K-fYoLY*L)aBJ#D~w&O~}xtJGDyHs#4XzL3L50e`H;s=~&O zN-Cml?!&YuCeN`)ikkNzg*1n4CJf`Namm(whJ@Q4_+Z67<#iBN1f^hh?gu3(!dUw& z5Jj5wY(+`LwLbh#8v03AaK&r(t7ZL47j7Mb`9{Mx_0N_#7A;9+PV6EkXKysQVdX4! zxTN`VMn`1ltjgU|5*cy=IC5(GjK?idJ|LnxQ!JrJ*MkhF4AkkoopWZY6pAEljgX_v z!p6+SsVWYUaaPi7)?R(c4Kw+!oi~FR?}}QTHS*K^+mr~D>#2GsJ5hc@aBQ!cl+%8<{7FN3Ozlz=XNA&3 z%X;|?G2%kZvD&{J@s{4Z034fTUda38bUE}x1P-04bDD<^>yFDsvuly1gL5TK3@#v% zan|MKJKYx(r6yV7o$rxc*h9fnO42Em^PyFu$KkY1pO-JasEjEMsb&vRy68VE8SH~9 z&u-psmn$%buWANAK|Z|OQ09AF z|2pUVHpTBe!Z>%vQ^6sr#yS8kB|=$UY&LVo$m|+fq}^*os;xh&_19Vnbpk$@KxY|V z`d9BU9j;nZZ+5(wYo_Nr?0q9|-@@Jh;;Db}rhwUbYI{F<)bnkQl|| zY%a0MYQTa~;!x)(gZWR&e-yJ8M?z9_1SfWMEt_K&w7=cTJH<>SZBF-fpPA5EMp~L)PpHykM zZTwS>3druX*{Zq)_ZJ+YN&2rTH_~_H0+FY~say9i)5gkKn?{`-LQK!}dlN`q%l!5T z>TqSrQ?9zofH_|=%u(q`1%J9Go@ny0L$iz6L>{=_3sy5lZ+}ss@SR0apWG{Xu`urP zu{>+E?&ZYf;Q-QmZ|lO$e3f1#nZbB-z zZz0jz>rcN8i6lT3Z6JDiO%bsn(Iq247j!-d&K6+IoFgojjC5&Y6_D@PBx3t)yM& zVe|rY%=;uf>Y9(K^@TL4vva&XV?Fk|^QX;CnR_LvIX{PT@n@IzRK{-}7K0@o?Yc&k zZLPFR?@wIR_qvgtu@p>1u8M|nWFvOal!TitVywwjewF97s`_Z9d|HY^NT9eLoGb|^ zR+4(y$DB`)}AVt(ji{(p~0%PJ`))XxYqcI%^-gl6yayFIl68fyV5a#K)qdDr_J=u__(nPRou|}UK4&ZG*zog8Z6&rG#lij@iWEC9QP$~J5u_nf-sKVJb8X7^z%qBeD z7PpU*iVhvVV{SM_c0z$Ph6Ng7+)2&yAhr(2Mcg4j3D5FX!aC1A@rt8r{kSufdxlJcmuOvK*5+}Q8^!8x`~>Ga`+-} z`s-iYc#LH~h@;k2(T1_R(@RTsQqRw8)W8nry|x`2psvs!dWm)4|0_1txY?>q!_BUg zJev@G*q;|W~qRQ&$O(N#At z`I&24?`vWi(KeFIK15)EtSWxgx^^eLAP7OXh`?G9A4SD(Pwz$&C-a8l;gXz$4U5h^&yM{f=3a1+TEF+2H2=9&`DG%9rv{-~)NzAJ zX8sowwK!Wf%D(H>Yq`lXv#Pv{xMS(}xc!`Ey|ufmx2aAc;SKR3D)*C0+0Jl0 zWkCjc615a(qA?Ln$f|zxogp0bp1}jhb9U&Ld8=^A@vC0uZfbx$thB+ngVM0c(6Ou! z7CZ?3FGjDLzr((5$D`|x8n<6jfhu2>E1nAO7m=6xah@cdE5h+YdNRrI{&ws}W~dwA zJ>VW0C=9K(@C`XIw_a(EUgbr(BhBM&(swPPrP-?+lZMv55pwQYkB)VfeugjRY~L^L zEu<%9a1I{5u*J-aLA^QZ49Vpez#F@)o#f7oo(pB% zflcoLPWj^P#7n?BqRLfRY41y(`2xRC#Fp$_pZYN}(mkV%E+4~a)0i_k`|r`M?W#eq z8)w9-TO(k=pg#UzIq@|UJ`RZF64!!FxDsJ^9XS)SMVMm;Ayj$zJK>>58T5n;T(xmQ znO{y+ol)t&-M1!9-Rr%d{eUT9CH-JLtfy&o}K_D`X@GFPhMU?=@u*!j(~ z={25rgMz80u0~ z*(uCBin61YWIMtdh*b zBQBN*J8zdd|OJ(o;eW~xX8}U99g8x*tC%-W(O{nX_`QZ?WB?^M`KJ+&ARslIZIQTV)W(xs^(3Z zY@_^N^(ata8(esqGT^-BtfJZo5&|BSy&`ghaZ4kiiXD%Vn>g1j5}El+l7fl(cy=sp zl7z9u-QVQ>a0<9rF=wUYmCCa)^sehh5Y z!=`j1K1qm3yNS!AR{A<2&eZ! zP-r=dipq+V@@6uCWqJxb?&cj-eAj|r_a=f z;ke+(h=wGc2AfGy0wYa?b}^jA8P6l!JMQ1pdPjl6=VG3hYM?xH9^ z2d(6k6`o82n~tV5r1`_X<)&zqR@8fWbyp*kZOMxT=7n@-I%mbIg&SDY+v?7T{0P^R zg)XsBuh5CYujl3_nC|~nR@6G3K4zXPt3`K0&3>9vGKjT@F!Aw-g@^*QLb&QqB8Qxl z8{}9wu=BpDicO#r*rucK(VpS{46x$Luh%Ithv>x9L;W3HMgBupkI|VL&|!H?EKcst zn=-XgV2I>i5DvM*q55RvMIl;yw6b|(MRr&>l|->uwP&-y{f@wB-um$fy|u7qJqMBKX{`R2^%{iapixq^yk3Q{*w%?9+#K zHxh}tO!Z4&10_`zY0h9fjJ#k}SSd-od3&wMt~1{H@uo-kT6?tE=w&5msi=OH>$s+K zpvU`|MT^Cw#CK;Mc4rWK9^m{=R5pQGQ{_Ued5TrJMReiQM%VqzJwnoNBF7dLxfp&hEp_$v*etg?8_7++O2-vIjF(X@vY8*yD%ntQ6gW z@%=98=u9IY%cTGD-Uz1>+|M7dqKm&e3AW!cE1GA{*IwyV65Pq|4g#6C3i}Iiey|a3MfVGo@B_*=ll!Kt#P+5br6` zdqQ4qOZB12i=%Y=$N3FzHQqr=_o_j~SAE;-)UE#UGsLV6@+7h(%`e{<&#`xl7pwK1 zkY21wuUMhr2hFn?z2ClB^4nsOO7~Zsn%PW91@Rw4uP^&$M!Z3tA{7j z*HUV|UT+lp+`XqjR_tGw{&UB;Uf~}=52qw=3qfhi@ ztzVD1-OrrQM6OHt@tFYS={2FnE?x^F)_&Qr%ZtuFf;MUo!vR|TO~)1i9@m?+ zUFX_!*o;GRaCYOg14WvH_D>CXY)?Gi^v13=bXtr0CyhlX-&n6@!@oNq7PrQo7P2Z}| zm*iXY4Keg_Ct5L$5JP zn{hcj5_XAP8&NOpE(znLxaNki`+chwnbmzsW9?LX>ZT;+oDm^yMrg9{mJeA~Egen8U z>E1^Xm=4)5j#ThTS`j->1^ih~)eR(gxYNi#9u!HdeWa>o|CHy${fhQyBI`_iqgs{% z3PP;+9QGh**PyJuS+{4m9YBO|l_xaY#hdqqw8!fMeqV);ia(tZU1b(nFdAhgO z&I+H#rqHWQ)q6>x^@a{_to!UFce=`Z%d%KmJOkLV=f2b{-k$iP;)_z29|>N{R`v_5 z58r8l`(m6gg!|ZT_%NB;FkTU@*nuv|V7%k|Y`e_3YOC4VE-bXU`^`b9<^-Ku^?^&JM1b<(k$W$x^**0gqedI*VfOF@-@*6w?8>!=Z^DnA}%czxtL6 zwj%*%Vmli5-;c#7%(NUc9If?fawxC9uW_PIo`mmfcwOzh#XOBGatd4FOFXddMZj&% z2+*R!q-WB3kKgXqHZ`M_k>Vk{(v{aR`o4NhH}GeV?Ao>h<+vjFHi^{Xq<&UZ>s06L z0;)d3_c)hslhZQ_QHEFAVsbA{>j!sp0XGJH6DZhv*z!(zgwNagMV|XI3ffzbUYT}< zzVqoBxvT=*`xJ)Ec?0LbDX7*Eg3MUc{(e}xd9CYYi9$Yc<}t}zT?Z)`I8X@B8;Y+(_~^J zd|IpQ-D-0Cs+Q`v_wzMh9!8vNRYudavr!YYdKFU|`)hI&eLTMKrp(*7w~6n?JaN#t zaQMhxwP!~JY#`0D3*x$|dcD&(+DW-%AJoJT-}x*~UI-*ety8S5GdvY-(r}X8;{j*l zy?ZApb3OCPPqk0s&FyO^j~K=DADi(h#^@VU6A|XH$!)vkNmc>*MHE7W@Ft_awMtF| zVI^G(FKORi*1w-g)JHxo^;PB27lJP7qqga3n7vuL?sv%{Uj6vRQSNB8Ww=Dd0DkZICRnFhJAVed{?l^&Mjnm!oGxSMhFVkvdA zYGb>`QpNj`Gk6zH5Oo5zF)a!ti+B4Y>BgOz;Gp5`?9tao%W%LU#~~}-dV%=TNBSmw z!O~e_Ru2RtM!rv`XVc5>GLx=}R+<&f2(rO`C(6}+{z3Yx2&Q^eq!&Moz>Ni3u`8u2 z%~ol2P7vblW9u#rc>aL+-goeZoA<|k_bd|);o6+gQRlHlFY0cFgq`3rx7IcfYK-$` zAo|r}$W&CO%nB_Y5@3U}R`OnTJ8yUd^y3GjwoDM{js1A&D~nR}uEYm1Zq7dN1HRAL zv?_Uh91a>Knv0QypKO#-&()EBE=-){!MNq%z7|}=$$JrAgE?<1U%shU3bL-=t>I!z zcYC#Rd{*{u@6C!6%om1UF_xOqmOBnNV{%7LlAZ+*3iR4%&hZ5s8S{peC95-0dR(AE zqu}BPIFH7yaqLf8vFYe$!WOKReX;H z)|?}&ur3q&5nT}r?!T>p!lC;h|9ISfoyet?V=owVIF&Ps-&;}#L{^*3G{_3+$R1oA zgd-mhx1KM1TLv|R@a}B_V!Wc+usJHd0k}`5wx=cbTkG~+uqmNHTwBj2eA}RE3HY29 zRKCkeSNtx}u1dOca~jqmTey5<63q&Z8=0u^Bmx+EYh0w#7bto(71(LVJr#UNj(TJ+ zb19x5^U8dpJ1JWOU6j_P&OYFSRcQAxs^nt6_TogJ7s&MO!7$Q@l|8gaBA*OT&a%}t zz_rrlXJeV$X@q6H|F>6kGUDvcgSm8?8Uli)KZWyktVtpUSonjjP3?Qe3mO1VHLX2V zF(-Z}+-^uI{Ac)WG_mBtp-%K2@z;bhZ5lxofgtn?nsFewZdWCSv)9kri6TRTh?q}pJ)r@g z*Ctfr9f!!UxTfF9u5XvW{pG4W#R_hZv6zQ9UGd};G~}*)v%nPIYia$#`MBfL$-e(Q zk=3G;XI=rifcAwy%_}FWHEtalq{U?aS~#IoPSvb8U%e1ADiiGW@OP|gx={CmG6=8u zqWJ}NYo%!UG}E;74Fkc|rUz8YsgCy9>Z2$*H|{;XEKBq6Qa2E2pQG7u zmvibG>&^C5bcbfB{sdUz19;;yupgCl&ek|wC9P93kc=tzef`LY!=^}@$ygSYpd+#xaar%Wj_*EdZM!`s#_g)f0L z@Oi%SUPlx*b{=tn;pt4Zwn{tJGGJTY-T58vcw>U@lIC6Xclla+SKIdBz;wGBM!CGN zVhAGROkaM99^AZ1RG!lidB>@ZWq2B+Q|haSM6i-nz%99=Z;2iwoRt z8(ok6;J2d>nYK5YTg63%Cy4=}B^dc!UIQ|r0kgO`x#ZCmTZFLpm`I}`KQT{e^` z8;+A=vxzlraP+=-gMonSzbMuo9`6iweT6+B;fU^^3iBdCz|%~q)F#g5Fr&G<_oc1M zX>(u6FWBt(Cij4`vNQgD+=_9D4vMQzmhX98bblDuOH-w$+ZN{f|$Q9B>R|y z?;0xSdcUN&pxxP1EvFD_Q5x4G*zLESH9iz`Xx;rPJ=^Yo(YQZhAneyDu7UYE;HMOy z1W;-*n}1J>0sg_?Adic9lPS2T8@oTY@!q3ZRlc9>z=DE=oP4=4x`6YLEU#!> zdS9tvIL0?TW12}}6mn@H>|S&MLnebXIhvXa$g>#dn2m>nN)5V9%agTsL~wPAdOz=e zY(WFd@7-QSIP%U1^`GT_lbCzgR~AP<624=QQfsgZy*JafmmStzPfsZ{qYz@<39pzx zx-N4mDpEZ@@J-ky`OsIT5#=>oYwi!$sv@J}Cr-dax1{ntZNp0ac#$GpP>&QHW|vj8 zZl?~suCG%2FA^$cN*y9&^w7^>%)+%3{T8kZ1SOTvgBa_Gd&?*bLB)3J?$rr|1Yl+zS6e=`@~_=+&YXg-QbjYl(y1M4@;s|s2K=2JQoCQ6 z47U@${`j#KI>CQ^`mQDBo&|$?2UX+(l#&56N`9A%aGAcZ$|s!c>kcWpEp~dPx{_Ze`x#8z`z7g-2NiKJ_P4}aCLVFt~-+U&1yao52^AG{{ffO zASuaD*45Xq+NGOSjt7|sEC-f9=bdnT5~hhPSdtOYaqpJX%jo(P*|s`Q4}W+nR>t>@ zId>fJm8-rxRb)w*qq+~#g(Uoz+|^9GfBD1YO(i5Ed-J$KV0AK( z!B35|{HxtFs%`L#+_A+W2!}LQN*|3QJFG+23Ry+4m`{Aus=5maKeU70@GNW7@x4K1 z$&lQXV&QTuffej251=k}(rBAyHq(_;qHJb6RW&C09&H$nsa zZ;<7gl@JEo8`_!KI9S_39*&2!|3D_vAEaevg=vMP6@ZZE=pl@d352NmqagU3rlDhI z1kf@4rU@7!NCFEZgsBm*H+%+DSUDh?Li#r1U^5d_NRu6e%x$}dAzJ-~g zfR%{_`1vt?MF+5@3d9VVLG3RaW)KiEgsHyWGqOR&pv0ihV8~zy0blePj2OTSrVM5b z77UgQRt(k*)>dE!8?c?3wGo3IgFS--gCq4HKOuk<7(zTf6Ci)1d4PXsMgGE~*gzcr z&sdbDvb6OocVeP)u@5=HV%yYATvE z*1MXa(XAdh*6S&%nN2eXRWwnWosCqb`gu-2G~DnMp#NNsIjEb#kmg!hZenX+v};N- zuK8v4v7hb+e5*KCEi1)sH)xq*x>wBA(oX$V$DwQ|-(srD(Hpb9CJGm^XEzt0{@s-} z(Z0Uk$?X~Q*Y?5%M}cn@J4q?rXb*Xc*Ds}XZ)eY??^+M7V`maCZd~RG^7XjY=+!Nq zYBDY!8#!LHJ`66$oj$rUOVyVHl$BMJ#%vW6y~e!00yaE)Mk}=7b4#9*BuzV=*c7lO zH|#wEX*A#qQ0d}<$nyr8Ek^kTjJ1JfRqUC5odWnMVb~kUxCmx`J!C@f=!bwVDL#vXhz1=8CRFpgZ~?k3cL z)Any84GY*sc9AhB>T+GHTN@^Nb^JD&4YRcwpqsN6lh<#Hyeh7Dx^fxkXGL(ElWpGb zi`{liq#z3?BhDjaBgpvP9g|&Nz$=54rU%dMSMv36)VdM^AE{R)FVc>K za{>EKhbzsBeOC5R1hS{qc>k8D2rnDWWu1KJP=o?6+oy`mbTRElF0aG0>`!yt1*fdC z-JNMOd`|qGe}hnPka`u=w+BD>K+v{-o*w@=Wgz_6 z?~;T7h<5f4&ovHUh8zh}`hPWmKsICsRWl<8Q+o)w^G8+5TG`6%nNS4$F5iFD^E1Hr zPw$?|K>KGj&=KMb#4kxRBM8~|`ylu`s0REyAo+aiL6oS3tqtiE9rW!Sr~%I)n*xB! z1~M8wkQ(p|#6g<&5RMLl_R&0xo`E$5$hjwFW@+{e5B}Zy?~4DJ^&emz*dAi{+mV07 z^mm5tPsWAxAxEaQ$uqYI3GRQe#XoKXfBW{QPa2XX24#D&tfPa4nbmV1o>@SF-%bPm zr1RfsBk=D_3nAo;91S5zD3!FCq!L&SU}Izq`GWAL%#8F*ERa)Gz|q0f+K!5vLCMU) z0u1?nCKDm~fL#4X9wB{F`p=fmW)Rra6tXMq?7+s<43c11XKOnndn(8n|0f$s$;e0v z(SyYCzv7dBsVLd$+t`4OAf(?j=m-g4Nkola6oMlf=v!Gm$Dw3uW)Fb;{fUR}Pdor- zdLRT`1W^4eF6zJC9b6#(B0~r{4rIpvc|lk_77z=-81O%`=LLIy0j&N<#>5I)ga43$ z*dZI_KVK-UR+j(LV`gRlPoE*UDI=uX|1mE_2D#n-r;LLca$o+J zo`an}u@u)7~1d@8q)kbr=&5R(AA zh=8aFD%-hVIHv%9*gyQ;dYXJFQkNjOW00P(k6-3uj#bKR>dGt?{!K+y7`I17%HY&792vU}iSJp9>kHZS8F02(hv@ za5fP&F|so@L53JRIXjvd*dV(vFpiEQ_kdo)t$Ae`#v*-BwD$FycJ7Oi>$a8uAt$=} z1TQ)@bpnQU!l@UC3njUe#N33RHIq#OgamEE3}PQ?tz`E(k8$xmPVI|qIA+mC1jwIiIbg+qmhXdfc1|+&cxWlK-kV5pbdfu z*ul&I4lZV0WQe&V#0~&{mO|`=Y;Elzqi6$Ip4$N3=S=)H^|R!kQ~y>*1}eK4I{$V> z#l+niDDr0pur2^70?DAWg`KS^B(VS*QEp}sGYg0f#KjC^Wo2Wh2Qh;{v;d&o|K24c z1OPLE06-N04MY(#kLB-&w+1$lz(wqAY@S{JckDEhCf3&SIv`e%o4k^&F~mgP-ozGQ zYGCaI(HDoL;yE!!&H!yND<=~NBnRxQY)s6Y05Cg_o&(b7WKb( zn1-en0L;S7#KF!9-~h2Pf!G1IF4oq6H{|&LF=Y8|2$_)y%*FX%mR$e4<-gqi?}-N z0CrBunm|^B8R7#6Co2;N;8~A_9n1s_jwiF2(OS~ zh07R$h;+2_lC`N7auh@H>Yp@$$ z(0h=RDlF><7~0w)f2>3FC(yu8^?)`+E@2pfGBsrvH#Jogm6mp~s$x8`1CtlL`8G7a zK=S2|6v6PVAofYR>=N9)X^}n#%wWrR7>(~x3>j?>8S$+fyJf|>UAYG`(JaBZyNPS?TT_T9`z>cOB^@fo3(WgTf^tVY(Gqh9-9vq_$$geks@uXANKH z#WOMT1vkgk&J+#K>t6i@p)Wn?yMJ={(CBFN#Fw$uiIpLx4@o3zrf3Z?pJ6in-&P1Z zb&aDm_fzc;lSEQztZ{vmsW7#Jc4cvZfmwlTm^BsraGoBDGD!Rr&Yg!i=M4ph zf+JMGBXiQz@OzRIH#;bXD26BWtHvu*|1u#z?(~$@`9-dj_q=67Nl++O+5kZ*r7X+? z^g_gx_ajdr)@qjqzjF=;w(15pun)^Mixc+y{4nIM&GN3!VJCK{8doSD7H;juM@8}} zQiGKi254ry+QwSgfUdDHzfEe}LpWA1t5(D*y@Ut28aG&v2Q-E

1409D}1DUHQ;8 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 QI9OTP;VCG@6(!*RAGC7CQ2+n{ diff --git a/src/test/search/resources/test-library-A.bib b/src/test/search/resources/test-library-A.bib deleted file mode 100644 index 267ae48d767..00000000000 --- a/src/test/search/resources/test-library-A.bib +++ /dev/null @@ -1,26 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {cASe}, -} - -@Misc{entry2, - author = {test}, - title = {casE}, -} - -@Misc{entry3, - author = {tESt}, - title = {Case}, -} - -@Misc{entry4, - author = {tesT}, - title = {CASE}, -} - -@Misc{entry5, - author = {TEST}, - title = {case}, -} - -@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/test/search/resources/test-library-B.bib b/src/test/search/resources/test-library-B.bib deleted file mode 100644 index ff076a5a52c..00000000000 --- a/src/test/search/resources/test-library-B.bib +++ /dev/null @@ -1,21 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {Case}, -} - -@Misc{entry2, - author = {User}, - title = {case}, -} - -@Misc{entry3, - author = {test}, - title = {text}, -} - -@Misc{entry4, - author = {Special}, - title = {192? title.}, -} - -@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/test/search/resources/test-library-C.bib b/src/test/search/resources/test-library-C.bib deleted file mode 100644 index 02f08f7b632..00000000000 --- a/src/test/search/resources/test-library-C.bib +++ /dev/null @@ -1,34 +0,0 @@ -@Misc{minimal1, - file = {:minimal.pdf:PDF}, -} - -@Misc{minimal2, - file = {:minimal1.pdf:PDF}, -} - -@Misc{minimal3, - file = {:minimal2.pdf:PDF}, -} - -@Misc{minimal-note1, - file = {:minimal-note.pdf:PDF}, -} - -@Misc{minimal-note2, - file = {:minimal-note1.pdf:PDF}, -} - -@Misc{minimal-note3, - file = {:minimal-note2.pdf:PDF}, -} - -@Comment{jabref-meta: databaseType:bibtex;} - -@Comment{jabref-meta: fileDirectory:.;} - -@Comment{jabref-meta: saveActions:disabled; -all-text-fields[identity] -date[normalize_date] -month[normalize_month] -pages[normalize_page_numbers] -;} diff --git a/src/test/search/resources/test-library-D.bib b/src/test/search/resources/test-library-D.bib deleted file mode 100644 index 6d78ef8dd5d..00000000000 --- a/src/test/search/resources/test-library-D.bib +++ /dev/null @@ -1,55 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {Case}, - groups = {A}, -} - -@Misc{entry2, - author = {TEST}, - title = {CASE}, - groups = {A}, -} - -@Misc{entry3, - author = {Hello}, - title = {World}, - groups = {A}, -} - -@Misc{entry4, - author = {HELLO}, - title = {WORLD}, - groups = {A}, -} - -@Misc{entry5, - author = {tesT}, - title = {casE}, - groups = {B}, -} - -@Misc{entry6, - author = {test}, - title = {case}, - groups = {B}, -} - -@Misc{entry7, - author = {hellO}, - title = {worlD}, - groups = {B}, -} - -@Misc{entry8, - author = {hello}, - title = {world}, - groups = {B}, -} - -@Comment{jabref-meta: databaseType:bibtex;} - -@Comment{jabref-meta: grouping: -0 AllEntriesGroup:; -1 StaticGroup:A\;0\;0\;0x8a8a8aff\;\;\;; -1 StaticGroup:B\;0\;1\;0x8a8a8aff\;\;\;; -}