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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