diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 11163bde525..6d62ca17488 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -31,7 +31,7 @@ // Install java. // See https://github.com/devcontainers/features/tree/main/src/java#options for details. "ghcr.io/devcontainers/features/java:1": { - "version": "20.0.2-tem", + "version": "21", "installGradle": false, "jdkDistro": "tem" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 67deb76ec92..bac31c8769a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We reintroduced the floating search in the main table. [#8963](https://github.com/JabRef/jabref/pull/8963) - We added the possibility to find (and add) papers that cite or are cited by a given paper. [#6187](https://github.com/JabRef/jabref/issues/6187) - We added an error-specific message for when a download from a URL fails. [#9826](https://github.com/JabRef/jabref/issues/9826) - We added support for customizing the citation command (e.g., `[@key1,@key2]`) when [pushing to external applications](https://docs.jabref.org/cite/pushtoapplications). [#10133](https://github.com/JabRef/jabref/issues/10133) @@ -27,6 +28,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed +- The search in the library now displays probable search hits instead of exact matches. Sorting by hit score can be done by the new score table column. [#8963](https://github.com/JabRef/jabref/pull/8963) - It is possible again to use "current table sort order" for the order of entries when saving. [#9869](https://github.com/JabRef/jabref/issues/9869) - Passwords can be stored in GNOME key ring. [#10274](https://github.com/JabRef/jabref/issues/10274) - We fixed an issue where groups based on an aux file could not be created due to an exception [#10350](https://github.com/JabRef/jabref/issues/10350) diff --git a/build.gradle b/build.gradle index 49a6162d17f..0154d3a096b 100644 --- a/build.gradle +++ b/build.gradle @@ -41,15 +41,15 @@ group = "org.jabref" version = project.findProperty('projVersion') ?: '100.0.0' java { - sourceCompatibility = JavaVersion.VERSION_19 - targetCompatibility = JavaVersion.VERSION_19 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 // Workaround needed for Eclipse, probably because of https://github.com/gradle/gradle/issues/16922 // Should be removed as soon as Gradle 7.0.1 is released ( https://github.com/gradle/gradle/issues/16922#issuecomment-828217060 ) modularity.inferModulePath.set(false) toolchain { // If this is updated, also update .devcontainer/devcontainer.json#L34 - languageVersion = JavaLanguageVersion.of(20) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java index 8eae854642c..e39b1dce706 100644 --- a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java +++ b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java @@ -3,10 +3,8 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; -import java.util.EnumSet; import java.util.List; import java.util.Random; -import java.util.stream.Collectors; import org.jabref.gui.Globals; import org.jabref.logic.bibtex.FieldPreferences; @@ -19,7 +17,6 @@ import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.layout.format.HTMLChars; import org.jabref.logic.layout.format.LatexToUnicodeFormatter; -import org.jabref.logic.search.SearchQuery; import org.jabref.logic.util.OS; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -33,7 +30,6 @@ import org.jabref.model.groups.KeywordGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; -import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.preferences.JabRefPreferences; import org.openjdk.jmh.Main; @@ -103,16 +99,14 @@ public String write() throws Exception { @Benchmark public List search() { - // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); - return database.getEntries().stream().filter(searchQuery::isMatch).collect(Collectors.toList()); + // FixMe: Create Benchmark for LuceneSearch + return null; } @Benchmark - public List parallelSearch() { - // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); - return database.getEntries().parallelStream().filter(searchQuery::isMatch).collect(Collectors.toList()); + public List index() { + // FixMe: Create Benchmark for LuceneIndexer + return null; } @Benchmark diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index fc4880d2562..29c7927ffda 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -50,9 +50,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.search.SearchQuery; +import org.jabref.logic.search.indexing.IndexingTaskManager; +import org.jabref.logic.search.indexing.LuceneIndexer; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.util.UpdateField; import org.jabref.logic.util.io.FileUtil; @@ -231,12 +231,11 @@ public void onDatabaseLoadingSucceed(ParserResult result) { feedData(context); - if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { - try { - indexingTaskManager.updateIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); - } catch (IOException e) { - LOGGER.error("Cannot access lucene index", e); - } + try { + indexingTaskManager.manageFulltextIndexAccordingToPrefs(LuceneIndexer.of(bibDatabaseContext, preferencesService)); + indexingTaskManager.updateIndex(LuceneIndexer.of(bibDatabaseContext, preferencesService)); + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); } } @@ -270,6 +269,9 @@ public void feedData(BibDatabaseContext bibDatabaseContextFromParserResult) { bibDatabaseContextFromParserResult.getDatabase().registerListener(this); bibDatabaseContextFromParserResult.getMetaData().registerListener(this); + if (this.tableModel != null) { + this.tableModel.removeBindings(); + } this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, stateManager); citationStyleCache = new CitationStyleCache(bibDatabaseContextFromParserResult); annotationCache = new FileAnnotationCache(bibDatabaseContextFromParserResult, preferencesService.getFilePreferences()); @@ -389,9 +391,7 @@ public void updateTabTitle(boolean isChanged) { setTooltip(new Tooltip(toolTipText.toString())); }); - if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { - indexingTaskManager.updateDatabaseName(tabTitle.toString()); - } + indexingTaskManager.updateDatabaseName(tabTitle.toString()); } @Subscribe @@ -904,7 +904,7 @@ public void listen(EntriesAddedEvent addedEntriesEvent) { // Automatically add new entries to the selected group (or set of groups) if (preferencesService.getGroupsPreferences().shouldAutoAssignGroup()) { - stateManager.getSelectedGroup(bibDatabaseContext).forEach( + stateManager.getSelectedGroups(bibDatabaseContext).forEach( selectedGroup -> selectedGroup.addEntriesToGroup(addedEntriesEvent.getBibEntries())); } } @@ -944,50 +944,40 @@ private class IndexUpdateListener { @Subscribe 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); - } - } catch (IOException e) { - LOGGER.error("Cannot access lucene index", e); + try { + LuceneIndexer luceneIndexer = LuceneIndexer.of(bibDatabaseContext, preferencesService); + for (BibEntry addedEntry : addedEntryEvent.getBibEntries()) { + indexingTaskManager.addToIndex(luceneIndexer, addedEntry); } + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); } } @Subscribe public void listen(EntriesRemovedEvent removedEntriesEvent) { - if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { - try { - PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); - for (BibEntry removedEntry : removedEntriesEvent.getBibEntries()) { - indexingTaskManager.removeFromIndex(pdfIndexer, removedEntry); - } - } catch (IOException e) { - LOGGER.error("Cannot access lucene index", e); + try { + LuceneIndexer luceneIndexer = LuceneIndexer.of(bibDatabaseContext, preferencesService); + for (BibEntry removedEntry : removedEntriesEvent.getBibEntries()) { + indexingTaskManager.removeFromIndex(luceneIndexer, removedEntry); } + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); } } @Subscribe public void listen(FieldChangedEvent fieldChangedEvent) { - if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { - if (fieldChangedEvent.getField().equals(StandardField.FILE)) { - List oldFileList = FileFieldParser.parse(fieldChangedEvent.getOldValue()); - List newFileList = FileFieldParser.parse(fieldChangedEvent.getNewValue()); - - List addedFiles = new ArrayList<>(newFileList); - addedFiles.remove(oldFileList); - List removedFiles = new ArrayList<>(oldFileList); - 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); - } catch (IOException e) { - LOGGER.warn("I/O error when writing lucene index", e); + for (BibEntry bibEntry : fieldChangedEvent.getBibEntries()) { + try { + List toRemoveList = new ArrayList<>(); + if (fieldChangedEvent.getField().equals(StandardField.FILE)) { + toRemoveList.addAll(FileFieldParser.parse(fieldChangedEvent.getOldValue())); + toRemoveList.removeAll(FileFieldParser.parse(fieldChangedEvent.getNewValue())); } + indexingTaskManager.updateIndex(LuceneIndexer.of(bibDatabaseContext, preferencesService), bibEntry, toRemoveList); + } catch (IOException e) { + LOGGER.warn("I/O error when writing lucene index", e); } } } diff --git a/src/main/java/org/jabref/gui/MainMenu.java b/src/main/java/org/jabref/gui/MainMenu.java index 28a08a31a0d..c7b7a25919f 100644 --- a/src/main/java/org/jabref/gui/MainMenu.java +++ b/src/main/java/org/jabref/gui/MainMenu.java @@ -283,7 +283,7 @@ private void createMenu() { new SeparatorMenuItem(), - factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, frame::getCurrentLibraryTab, dialogService, preferencesService.getFilePreferences(), taskExecutor)) + factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, frame::getCurrentLibraryTab, dialogService, preferencesService, preferencesService.getFilePreferences(), taskExecutor)) ); SidePaneType webSearchPane = SidePaneType.WEB_SEARCH; SidePaneType groupsPane = SidePaneType.GROUPS; diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index f1ff42c5785..14694a2caa2 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -2,17 +2,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javafx.beans.Observable; import javafx.beans.binding.Bindings; -import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -31,6 +30,7 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; +import org.jabref.model.pdf.search.LuceneSearchResults; import org.jabref.model.util.OptionalUtil; import com.tobiasdiez.easybind.EasyBind; @@ -60,7 +60,7 @@ public class StateManager { private final ObservableList selectedEntries = FXCollections.observableArrayList(); private final ObservableMap> selectedGroups = FXCollections.observableHashMap(); private final OptionalObjectProperty activeSearchQuery = OptionalObjectProperty.empty(); - private final ObservableMap searchResultMap = FXCollections.observableHashMap(); + private final ObservableMap> searchResults = FXCollections.observableHashMap(); private final OptionalObjectProperty focusOwner = OptionalObjectProperty.empty(); private final ObservableList, Task>> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[]{task.getValue().progressProperty(), task.getValue().runningProperty()}); private final EasyBinding anyTaskRunning = EasyBind.reduce(backgroundTasks, tasks -> tasks.map(Pair::getValue).anyMatch(Task::isRunning)); @@ -97,14 +97,6 @@ public OptionalObjectProperty activeSearchQueryProperty() { return activeSearchQuery; } - public void setActiveSearchResultSize(BibDatabaseContext database, IntegerProperty resultSize) { - searchResultMap.put(database, resultSize); - } - - public IntegerProperty getSearchResultSize() { - return searchResultMap.getOrDefault(activeDatabase.getValue().orElse(new BibDatabaseContext()), new SimpleIntegerProperty(0)); - } - public ReadOnlyListProperty activeGroupProperty() { return activeGroups.getReadOnlyProperty(); } @@ -122,7 +114,7 @@ public void setSelectedGroups(BibDatabaseContext database, List n selectedGroups.put(database, FXCollections.observableArrayList(newSelectedGroups)); } - public ObservableList getSelectedGroup(BibDatabaseContext database) { + public ObservableList getSelectedGroups(BibDatabaseContext database) { ObservableList selectedGroupsForDatabase = selectedGroups.get(database); return selectedGroupsForDatabase != null ? selectedGroupsForDatabase : FXCollections.observableArrayList(); } @@ -205,6 +197,10 @@ public void setLastAutomaticFieldEditorEdit(LastAutomaticFieldEditorEdit automat lastAutomaticFieldEditorEditProperty().set(automaticFieldEditorEdit); } + public ObservableMap> getSearchResults() { + return searchResults; + } + public List collectAllDatabasePaths() { List list = new ArrayList<>(); getOpenDatabases().stream() diff --git a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java index 269ab381a83..04e53d76b7f 100644 --- a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.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.search.indexing.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; @@ -35,9 +35,9 @@ public CommentsTab(PreferencesService preferences, DialogService dialogService, StateManager stateManager, ThemeManager themeManager, - IndexingTaskManager indexingTaskManager, TaskExecutor taskExecutor, - JournalAbbreviationRepository journalAbbreviationRepository) { + JournalAbbreviationRepository journalAbbreviationRepository, + IndexingTaskManager indexingTaskManager) { super( false, databaseContext, @@ -49,8 +49,7 @@ public CommentsTab(PreferencesService preferences, themeManager, taskExecutor, journalAbbreviationRepository, - indexingTaskManager - ); + indexingTaskManager); this.defaultOwner = preferences.getOwnerPreferences().getDefaultOwner(); setText(Localization.lang("Comments")); setGraphic(IconTheme.JabRefIcons.COMMENT.getGraphicNode()); diff --git a/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java index a40302d54c6..db6a4c35d4a 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.search.indexing.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/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 3d42d562a26..31ceae355bf 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -123,7 +123,7 @@ public EntryEditor(LibraryTab libraryTab) { .load(); this.entryEditorPreferences = preferencesService.getEntryEditorPreferences(); - this.fileLinker = new ExternalFilesEntryLinker(preferencesService.getFilePreferences(), databaseContext, dialogService); + this.fileLinker = new ExternalFilesEntryLinker(preferencesService, preferencesService.getFilePreferences(), databaseContext, dialogService); EasyBind.subscribe(tabbed.getSelectionModel().selectedItemProperty(), tab -> { EntryEditorTab activeTab = (EntryEditorTab) tab; @@ -257,7 +257,7 @@ private List createTabs() { entryEditorTabs.add(new OtherFieldsTab(databaseContext, libraryTab.getSuggestionProviders(), undoManager, dialogService, preferencesService, stateManager, themeManager, libraryTab.getIndexingTaskManager(), bibEntryTypesManager, taskExecutor, journalAbbreviationRepository)); // Comment Tab: Tab for general and user-specific comments - entryEditorTabs.add(new CommentsTab(preferencesService, databaseContext, libraryTab.getSuggestionProviders(), undoManager, dialogService, stateManager, themeManager, libraryTab.getIndexingTaskManager(), taskExecutor, journalAbbreviationRepository)); + entryEditorTabs.add(new CommentsTab(preferencesService, databaseContext, libraryTab.getSuggestionProviders(), undoManager, dialogService, stateManager, themeManager, taskExecutor, journalAbbreviationRepository, libraryTab.getIndexingTaskManager())); // General fields from preferences // First, remove all tabs that are already handled above or below; except for the source tab (which has different titles for BibTeX and BibLaTeX mode) @@ -300,7 +300,7 @@ private List createTabs() { entryEditorTabs.add(new LatexCitationsTab(databaseContext, preferencesService, taskExecutor, dialogService)); - entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor)); + entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, databaseContext, taskExecutor)); return entryEditorTabs; } diff --git a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java index 780af05c3c4..ee50a980cd3 100644 --- a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java @@ -33,7 +33,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.search.indexing.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..80c437628f5 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.search.indexing.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..a2bb91712da 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.search.indexing.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 edb22834ff2..2320e2c5da2 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.search.indexing.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 dae09c7a2a3..708566858c9 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.search.indexing.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..80f3f73ade1 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.search.indexing.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 cfc530c9d08..9e6dc24d5a8 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.search.indexing.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/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index bd39e442b1d..9abcb4baf37 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -42,7 +42,6 @@ import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.search.SearchQuery; import org.jabref.logic.util.OS; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -122,7 +121,7 @@ public SourceTab(BibDatabaseContext bibDatabaseContext, this.keyBindingRepository = keyBindingRepository; stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> { - searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); + // searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); TODO btut: Pattern-Highlighting with lucene highlightSearchPattern(); }); } diff --git a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java index aa4e65b48e3..d467a5855f3 100644 --- a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.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.search.indexing.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/fileannotationtab/FulltextSearchResultsTab.java b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java index 00f7612f6fb..26ab054e42e 100644 --- a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java @@ -27,11 +27,12 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; -import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.LuceneSearchResults; import org.jabref.model.pdf.search.SearchResult; -import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; @@ -46,6 +47,7 @@ public class FulltextSearchResultsTab extends EntryEditorTab { private final PreferencesService preferencesService; private final DialogService dialogService; private final ActionFactory actionFactory; + private final BibDatabaseContext context; private final TaskExecutor taskExecutor; private final TextFlow content; @@ -57,10 +59,12 @@ public class FulltextSearchResultsTab extends EntryEditorTab { public FulltextSearchResultsTab(StateManager stateManager, PreferencesService preferencesService, DialogService dialogService, + BibDatabaseContext context, TaskExecutor taskExecutor) { this.stateManager = stateManager; this.preferencesService = preferencesService; this.dialogService = dialogService; + this.context = context; this.actionFactory = new ActionFactory(preferencesService.getKeyBindingRepository()); this.taskExecutor = taskExecutor; @@ -77,8 +81,8 @@ public FulltextSearchResultsTab(StateManager stateManager, public boolean shouldShow(BibEntry entry) { return this.stateManager.activeSearchQueryProperty().isPresent().get() && this.stateManager.activeSearchQueryProperty().get().isPresent() && - this.stateManager.activeSearchQueryProperty().get().get().getSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT) && - this.stateManager.activeSearchQueryProperty().get().get().getQuery().length() > 0; + this.stateManager.activeSearchQueryProperty().get().get().getSearchFlags().contains(SearchFlags.FULLTEXT) && + this.stateManager.activeSearchQueryProperty().get().get().toString().length() > 0; } @Override @@ -90,10 +94,15 @@ protected void bindToEntry(BibEntry entry) { documentViewerView = new DocumentViewerView(); } this.entry = entry; - PdfSearchResults searchResults = stateManager.activeSearchQueryProperty().get().get().getRule().getFulltextResults(stateManager.activeSearchQueryProperty().get().get().getQuery(), entry); content.getChildren().clear(); + if (!stateManager.getSearchResults().containsKey(context)) { + return; + } + + LuceneSearchResults searchResults = stateManager.getSearchResults().get(context).get(entry); + if (searchResults.numSearchResults() == 0) { content.getChildren().add(new Text(Localization.lang("No search matches."))); } diff --git a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java index 57df1e395c8..81733ee3d19 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java +++ b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java @@ -15,14 +15,15 @@ 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.search.indexing.IndexingTaskManager; +import org.jabref.logic.search.indexing.LuceneIndexer; import org.jabref.logic.util.io.FileNameCleaner; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,13 +32,15 @@ public class ExternalFilesEntryLinker { private static final Logger LOGGER = LoggerFactory.getLogger(ExternalFilesEntryLinker.class); + private final PreferencesService preferencesService; private final FilePreferences filePreferences; private final BibDatabaseContext bibDatabaseContext; private final MoveFilesCleanup moveFilesCleanup; private final RenamePdfCleanup renameFilesCleanup; private final DialogService dialogService; - public ExternalFilesEntryLinker(FilePreferences filePreferences, BibDatabaseContext bibDatabaseContext, DialogService dialogService) { + public ExternalFilesEntryLinker(PreferencesService preferencesService, FilePreferences filePreferences, BibDatabaseContext bibDatabaseContext, DialogService dialogService) { + this.preferencesService = preferencesService; this.filePreferences = filePreferences; this.bibDatabaseContext = bibDatabaseContext; this.moveFilesCleanup = new MoveFilesCleanup(bibDatabaseContext, filePreferences); @@ -87,7 +90,7 @@ public void moveFilesToFileDirRenameAndAddToEntry(BibEntry entry, List fil } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(LuceneIndexer.of(bibDatabaseContext, preferencesService), entry); } catch (IOException e) { LOGGER.error("Could not access Fulltext-Index", e); } @@ -105,7 +108,7 @@ public void copyFilesToFileDirAndAddToEntry(BibEntry entry, List files, In } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(LuceneIndexer.of(bibDatabaseContext, preferencesService), entry); } catch (IOException e) { LOGGER.error("Could not access Fulltext-Index", e); } diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 31edf671e97..d3c4320e587 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -83,8 +83,9 @@ public ImportHandler(BibDatabaseContext database, this.dialogService = dialogService; this.taskExecutor = taskExecutor; - this.linker = new ExternalFilesEntryLinker(preferencesService.getFilePreferences(), database, dialogService); - this.contentImporter = new ExternalFilesContentImporter(preferencesService.getImportFormatPreferences()); + this.linker = new ExternalFilesEntryLinker(preferencesService, preferencesService.getFilePreferences(), database, dialogService); + this.contentImporter = new ExternalFilesContentImporter( + preferencesService.getImportFormatPreferences()); this.undoManager = undoManager; } @@ -190,7 +191,7 @@ public void importEntries(List entries) { } // Add to group - addToGroups(entries, stateManager.getSelectedGroup(bibDatabaseContext)); + addToGroups(entries, stateManager.getSelectedGroups(bibDatabaseContext)); } public void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext, BibEntry entry) { @@ -229,7 +230,7 @@ public void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext, preferencesService.getOwnerPreferences(), preferencesService.getTimestampPreferences()); - addToGroups(List.of(entry), stateManager.getSelectedGroup(this.bibDatabaseContext)); + addToGroups(List.of(entry), stateManager.getSelectedGroups(this.bibDatabaseContext)); if (preferencesService.getFilePreferences().shouldDownloadLinkedFiles()) { entry.getFiles().stream().filter(LinkedFile::isOnlineLink).forEach(linkedFile -> diff --git a/src/main/java/org/jabref/gui/groups/GroupDialog.fxml b/src/main/java/org/jabref/gui/groups/GroupDialog.fxml index 3d12f80abbc..a4936953876 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialog.fxml +++ b/src/main/java/org/jabref/gui/groups/GroupDialog.fxml @@ -124,7 +124,6 @@ - diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/src/main/java/org/jabref/gui/groups/GroupDialogView.java index 0a7c76d0fe4..0c9804f3ce4 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogView.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogView.java @@ -36,7 +36,6 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.groups.AbstractGroup; import org.jabref.model.groups.GroupHierarchyType; -import org.jabref.model.search.rules.SearchRules; import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -76,7 +75,6 @@ public class GroupDialogView extends BaseDialog { @FXML private CheckBox keywordGroupRegex; @FXML private TextField searchGroupSearchTerm; - @FXML private CheckBox searchGroupCaseSensitive; @FXML private CheckBox searchGroupRegex; @FXML private RadioButton autoGroupKeywordsOption; @@ -164,23 +162,13 @@ public void initialize() { keywordGroupRegex.selectedProperty().bindBidirectional(viewModel.keywordGroupRegexProperty()); searchGroupSearchTerm.textProperty().bindBidirectional(viewModel.searchGroupSearchTermProperty()); - searchGroupCaseSensitive.setSelected(viewModel.searchFlagsProperty().getValue().contains(SearchFlags.CASE_SENSITIVE)); - searchGroupCaseSensitive.selectedProperty().addListener((observable, oldValue, newValue) -> { - EnumSet searchFlags = viewModel.searchFlagsProperty().get(); - if (newValue) { - searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); - } else { - searchFlags.remove(SearchRules.SearchFlags.CASE_SENSITIVE); - } - viewModel.searchFlagsProperty().set(searchFlags); - }); searchGroupRegex.setSelected(viewModel.searchFlagsProperty().getValue().contains(SearchFlags.REGULAR_EXPRESSION)); searchGroupRegex.selectedProperty().addListener((observable, oldValue, newValue) -> { EnumSet searchFlags = viewModel.searchFlagsProperty().get(); if (newValue) { - searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + searchFlags.add(SearchFlags.REGULAR_EXPRESSION); } else { - searchFlags.remove(SearchRules.SearchFlags.REGULAR_EXPRESSION); + searchFlags.remove(SearchFlags.REGULAR_EXPRESSION); } viewModel.searchFlagsProperty().set(searchFlags); }); diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java index 72f98979fc0..16b142e947e 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java @@ -48,7 +48,6 @@ import org.jabref.model.groups.TexGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; -import org.jabref.model.search.rules.SearchRules; import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; @@ -205,10 +204,6 @@ private void setupValidation() { searchRegexValidator = new FunctionBasedValidator<>( searchGroupSearchTermProperty, input -> { - if (!searchFlagsProperty.getValue().contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return true; - } - if (StringUtil.isNullOrEmpty(input)) { return false; } diff --git a/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java index 022941a97d5..b623055c1e2 100644 --- a/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java @@ -1,5 +1,6 @@ package org.jabref.gui.groups; +import javafx.collections.ObservableSet; import javafx.scene.Node; import javafx.scene.control.Tooltip; @@ -8,29 +9,25 @@ public class GroupModeViewModel { - private final GroupViewMode mode; + private final ObservableSet mode; - public GroupModeViewModel(GroupViewMode mode) { + public GroupModeViewModel(ObservableSet mode) { this.mode = mode; } public Node getUnionIntersectionGraphic() { - if (mode == GroupViewMode.UNION) { - return JabRefIcons.GROUP_UNION.getGraphicNode(); - } else if (mode == GroupViewMode.INTERSECTION) { + if (mode.contains(GroupViewMode.INTERSECTION)) { return JabRefIcons.GROUP_INTERSECTION.getGraphicNode(); + } else { + return JabRefIcons.GROUP_UNION.getGraphicNode(); } - - // As there is no concept like an empty node/icon, we return simply the other icon - return JabRefIcons.GROUP_INTERSECTION.getGraphicNode(); } public Tooltip getUnionIntersectionTooltip() { - if (mode == GroupViewMode.UNION) { + if (mode.contains(GroupViewMode.INTERSECTION)) { return new Tooltip(Localization.lang("Toggle intersection")); - } else if (mode == GroupViewMode.INTERSECTION) { + } else { return new Tooltip(Localization.lang("Toggle union")); } - return new Tooltip(); } } diff --git a/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java index bd113bf249e..dccd1fef498 100644 --- a/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java @@ -262,7 +262,7 @@ private void refreshGroup() { DefaultTaskExecutor.runInJavaFXThread(() -> { updateMatchedEntries(); // Update the entries matched by the group // "Re-add" to the selected groups if it were selected, this refreshes the entries the user views - ObservableList selectedGroups = this.stateManager.getSelectedGroup(this.databaseContext); + ObservableList selectedGroups = this.stateManager.getSelectedGroups(this.databaseContext); if (selectedGroups.remove(this.groupNode)) { selectedGroups.add(this.groupNode); } diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java index 8877939df48..f35df11f7dc 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java @@ -140,11 +140,11 @@ private void onActiveDatabaseChanged(Optional newDatabase) { .orElse(GroupNodeViewModel.getAllEntriesGroup(newDatabase.get(), stateManager, taskExecutor, localDragboard, preferences)); rootGroup.setValue(newRoot); - if (stateManager.getSelectedGroup(newDatabase.get()).isEmpty()) { + if (stateManager.getSelectedGroups(newDatabase.get()).isEmpty()) { stateManager.setSelectedGroups(newDatabase.get(), Collections.singletonList(newRoot.getGroupNode())); } selectedGroups.setAll( - stateManager.getSelectedGroup(newDatabase.get()).stream() + stateManager.getSelectedGroups(newDatabase.get()).stream() .map(selectedGroup -> new GroupNodeViewModel(newDatabase.get(), stateManager, taskExecutor, selectedGroup, localDragboard, preferences)) .collect(Collectors.toList())); } else { diff --git a/src/main/java/org/jabref/gui/groups/GroupViewMode.java b/src/main/java/org/jabref/gui/groups/GroupViewMode.java index 564c85496e6..b9b96cce436 100644 --- a/src/main/java/org/jabref/gui/groups/GroupViewMode.java +++ b/src/main/java/org/jabref/gui/groups/GroupViewMode.java @@ -1,4 +1,4 @@ package org.jabref.gui.groups; -public enum GroupViewMode { INTERSECTION, UNION } +public enum GroupViewMode { INTERSECTION, FILTER, INVERT } diff --git a/src/main/java/org/jabref/gui/groups/GroupsPreferences.java b/src/main/java/org/jabref/gui/groups/GroupsPreferences.java index e83d5546daa..1a7da04eb90 100644 --- a/src/main/java/org/jabref/gui/groups/GroupsPreferences.java +++ b/src/main/java/org/jabref/gui/groups/GroupsPreferences.java @@ -1,40 +1,70 @@ package org.jabref.gui.groups; +import java.util.EnumSet; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SetProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleSetProperty; +import javafx.collections.FXCollections; import org.jabref.model.groups.GroupHierarchyType; public class GroupsPreferences { - private final ObjectProperty groupViewMode; - private final BooleanProperty shouldAutoAssignGroup; - private final BooleanProperty shouldDisplayGroupCount; - private final ObjectProperty defaultHierarchicalContext; + private final SetProperty groupViewMode = new SimpleSetProperty<>(FXCollections.observableSet()); + private final BooleanProperty shouldAutoAssignGroup = new SimpleBooleanProperty(); + private final BooleanProperty shouldDisplayGroupCount = new SimpleBooleanProperty(); + private final ObjectProperty defaultHierarchicalContext = new SimpleObjectProperty<>(); - public GroupsPreferences(GroupViewMode groupViewMode, + public GroupsPreferences(boolean viewModeIntersection, + boolean viewModeFilter, + boolean viewModeInvert, boolean shouldAutoAssignGroup, boolean shouldDisplayGroupCount, GroupHierarchyType defaultHierarchicalContext) { + if (viewModeIntersection) { + this.groupViewMode.add(GroupViewMode.INTERSECTION); + } + if (viewModeFilter) { + this.groupViewMode.add(GroupViewMode.FILTER); + } + if (viewModeInvert) { + this.groupViewMode.add(GroupViewMode.INVERT); + } + this.shouldAutoAssignGroup.set(shouldAutoAssignGroup); + this.shouldDisplayGroupCount.set(shouldDisplayGroupCount); + } - this.groupViewMode = new SimpleObjectProperty<>(groupViewMode); - this.shouldAutoAssignGroup = new SimpleBooleanProperty(shouldAutoAssignGroup); - this.shouldDisplayGroupCount = new SimpleBooleanProperty(shouldDisplayGroupCount); - this.defaultHierarchicalContext = new SimpleObjectProperty<>(defaultHierarchicalContext); + public GroupsPreferences(EnumSet groupViewModes, + boolean shouldAutoAssignGroup, + boolean shouldDisplayGroupCount, + GroupHierarchyType defaultHierarchicalContext) { + this.groupViewMode.addAll(groupViewModes); + this.shouldAutoAssignGroup.set(shouldAutoAssignGroup); + this.shouldDisplayGroupCount.set(shouldDisplayGroupCount); + this.defaultHierarchicalContext.set(defaultHierarchicalContext); } - public GroupViewMode getGroupViewMode() { - return groupViewMode.getValue(); + public EnumSet getGroupViewMode() { + if (groupViewMode.isEmpty()) { + return EnumSet.noneOf(GroupViewMode.class); + } + return EnumSet.copyOf(groupViewMode); } - public ObjectProperty groupViewModeProperty() { + public SetProperty groupViewModeProperty() { return groupViewMode; } - public void setGroupViewMode(GroupViewMode groupViewMode) { - this.groupViewMode.set(groupViewMode); + public void setGroupViewMode(GroupViewMode mode, boolean value) { + if (groupViewMode.contains(mode) && !value) { + groupViewMode.remove(mode); + } else if (!groupViewMode.contains(mode) && value) { + groupViewMode.add(mode); + } } public boolean shouldAutoAssignGroup() { diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index 9833e46049c..821768d57b0 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -284,9 +284,11 @@ public enum JabRefIcons implements JabRefIcon { CHECK(MaterialDesignC.CHECK), WARNING(MaterialDesignA.ALERT), ERROR(MaterialDesignA.ALERT_CIRCLE), - CASE_SENSITIVE(MaterialDesignA.ALPHABETICAL), REG_EX(MaterialDesignR.REGEX), FULLTEXT(MaterialDesignF.FILE_EYE), + FILTER(MaterialDesignF.FILTER), + INVERT(MaterialDesignI.INVERT_COLORS), + SORT_BY_SCORE(MaterialDesignS.SORT_VARIANT), CONSOLE(MaterialDesignC.CONSOLE), FORUM(MaterialDesignF.FORUM), FACEBOOK(MaterialDesignF.FACEBOOK), diff --git a/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java b/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java index 6f614cb2679..e4469283075 100644 --- a/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java +++ b/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java @@ -13,10 +13,14 @@ import javafx.beans.Observable; import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; +import javafx.beans.property.FloatProperty; import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.SimpleFloatProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.MapChangeListener; +import org.jabref.gui.StateManager; import org.jabref.gui.specialfields.SpecialFieldValueViewModel; import org.jabref.gui.util.uithreadaware.UiThreadBinding; import org.jabref.logic.importer.util.FileFieldParser; @@ -29,6 +33,7 @@ import org.jabref.model.entry.field.StandardField; import org.jabref.model.groups.AbstractGroup; import org.jabref.model.groups.GroupTreeNode; +import org.jabref.model.pdf.search.LuceneSearchResults; import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyBinding; @@ -44,8 +49,11 @@ public class BibEntryTableViewModel { private final EasyBinding> linkedIdentifiers; private final Binding> matchedGroups; private final BibDatabaseContext bibDatabaseContext; + private final StateManager stateManager; - public BibEntryTableViewModel(BibEntry entry, BibDatabaseContext bibDatabaseContext, ObservableValue fieldValueFormatter) { + private final FloatProperty searchScore = new SimpleFloatProperty(0); + + public BibEntryTableViewModel(BibEntry entry, BibDatabaseContext bibDatabaseContext, ObservableValue fieldValueFormatter, StateManager stateManager) { this.entry = entry; this.fieldValueFormatter = fieldValueFormatter; @@ -53,6 +61,12 @@ public BibEntryTableViewModel(BibEntry entry, BibDatabaseContext bibDatabaseCont this.linkedIdentifiers = createLinkedIdentifiersBinding(entry); this.matchedGroups = createMatchedGroupsBinding(bibDatabaseContext, entry); this.bibDatabaseContext = bibDatabaseContext; + this.stateManager = stateManager; + + updateSearchScore(); + stateManager.getSearchResults().addListener((MapChangeListener>) change -> { + updateSearchScore(); + }); } private static EasyBinding> createLinkedIdentifiersBinding(BibEntry entry) { @@ -145,4 +159,20 @@ public ObservableValue getFields(OrFields fields) { public StringProperty bibDatabaseContextProperty() { return new ReadOnlyStringWrapper(bibDatabaseContext.getDatabasePath().map(Path::toString).orElse("")); } + + public float getSearchScore() { + return searchScore.get(); + } + + public FloatProperty searchScoreProperty() { + return searchScore; + } + + public void updateSearchScore() { + if (stateManager.getSearchResults().containsKey(bibDatabaseContext) && stateManager.getSearchResults().get(bibDatabaseContext).containsKey(entry)) { + searchScore.set(stateManager.getSearchResults().get(bibDatabaseContext).get(entry).getSearchScore()); + } else { + searchScore.set(0); + } + } } diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.css b/src/main/java/org/jabref/gui/maintable/MainTable.css index 51059819604..7734dd76b1c 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.css +++ b/src/main/java/org/jabref/gui/maintable/MainTable.css @@ -36,6 +36,14 @@ -fx-padding: -2 0 0 0; } +.table-row-cell:entry-not-matching-search { + -fx-opacity: 35%; +} + +.table-row-cell:entry-not-matching-groups { + -fx-background-color: -jr-gray-2; +} + .rating > .container { -fx-spacing: 2; } @@ -57,3 +65,4 @@ .rating > .container > .button:hover { -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.6), 8, 0.0, 0, 0); } + diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index 7c4e2b2e329..3011de7f78b 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -11,6 +11,8 @@ import javax.swing.undo.UndoManager; import javafx.collections.ListChangeListener; +import javafx.collections.SetChangeListener; +import javafx.css.PseudoClass; import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; @@ -33,6 +35,7 @@ import org.jabref.gui.actions.StandardActions; import org.jabref.gui.edit.EditAction; import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.groups.GroupViewMode; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.maintable.columns.LibraryColumn; @@ -50,10 +53,13 @@ import org.jabref.model.database.event.EntriesAddedEvent; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; import com.google.common.eventbus.Subscribe; +import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.EasyBinding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +70,7 @@ public class MainTable extends TableView { private final LibraryTab libraryTab; private final DialogService dialogService; private final StateManager stateManager; + private final PreferencesService preferencesService; private final BibDatabaseContext database; private final MainTableDataModel model; @@ -91,6 +98,7 @@ public MainTable(MainTableDataModel model, this.libraryTab = libraryTab; this.dialogService = dialogService; this.stateManager = stateManager; + this.preferencesService = preferencesService; this.database = Objects.requireNonNull(database); this.model = model; this.clipBoardManager = clipBoardManager; @@ -142,6 +150,20 @@ public MainTable(MainTableDataModel model, taskExecutor, Globals.journalAbbreviationRepository, entryTypesManager)) + .withPseudoClass(PseudoClass.getPseudoClass("entry-not-matching-search"), entry -> stateManager.activeSearchQueryProperty().isPresent().and(entry.searchScoreProperty().isEqualTo(0))) + .withPseudoClass(PseudoClass.getPseudoClass("entry-not-matching-groups"), entry -> { + EasyBinding matchesGroup = EasyBind.combine( + stateManager.activeGroupProperty(), + preferencesService.getGroupsPreferences().groupViewModeProperty(), + (groups, viewMode) -> { + if (viewMode.contains(GroupViewMode.FILTER)) { + return Boolean.valueOf(false); + } + MainTableDataModel.updateSearchGroups(stateManager, database); + return Boolean.valueOf(preferencesService.getGroupsPreferences().getGroupViewMode().contains(GroupViewMode.INVERT) ^ !MainTableDataModel.createGroupMatcher(stateManager.activeGroupProperty(), preferencesService.getGroupsPreferences()).map(matcher -> matcher.isMatch(entry.getEntry())).orElse(true)); + }); + return matchesGroup; + }) .setOnDragDetected(this::handleOnDragDetected) .setOnDragDropped(this::handleOnDragDropped) .setOnDragOver(this::handleOnDragOver) @@ -151,15 +173,39 @@ public MainTable(MainTableDataModel model, this.getSortOrder().clear(); + // always sort by score first. If no search is ongoing, it will be equal for all columns. + ListChangeListener> scoreSortOderPrioritizer = new ListChangeListener>() { + @Override + public void onChanged(Change> c) { + getSortOrder().removeListener(this); + updateSortOrder(); + getSortOrder().addListener(this); + } + }; + + preferencesService.getSearchPreferences().getObservableSearchFlags().addListener(new SetChangeListener() { + @Override + public void onChanged(Change change) { + getSortOrder().removeListener(scoreSortOderPrioritizer); + updateSortOrder(); + getSortOrder().addListener(scoreSortOderPrioritizer); + } + }); + mainTablePreferences.getColumnPreferences().getColumnSortOrder().forEach(columnModel -> this.getColumns().stream() .map(column -> (MainTableColumn) column) .filter(column -> column.getModel().equals(columnModel)) + .filter(column -> !column.getModel().getType().equals(MainTableColumnModel.Type.SCORE)) .findFirst() .ifPresent(column -> { LOGGER.debug("Adding sort order for col {} ", column); this.getSortOrder().add(column); })); + this.getSortOrder().addListener(scoreSortOderPrioritizer); + + // Is this always called after the search is done? + stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> sort()); if (mainTablePreferences.getResizeColumnsToFit()) { this.setColumnResizePolicy(new SmartConstrainedResizePolicy()); @@ -201,6 +247,13 @@ public MainTable(MainTableDataModel model, new MainTableHeaderContextMenu(this, rightClickMenuFactory).show(true); } + private void updateSortOrder() { + getSortOrder().removeAll(getColumns().get(0)); + if (preferencesService.getSearchPreferences().isSortByScore()) { + getSortOrder().add(0, getColumns().get(0)); + } + } + /** * This is called, if a user starts typing some characters into the keyboard with focus on main table. The {@link MainTable} will scroll to the cell with the same starting column value and typed string * diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java index fba775083a9..58f21e004db 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java @@ -124,6 +124,8 @@ public MainTableColumnFactory(BibDatabaseContext database, public List> createColumns() { List> columns = new ArrayList<>(); + columns.add(createScoreColumn(new MainTableColumnModel(MainTableColumnModel.Type.SCORE))); + columnPreferences.getColumns().forEach(column -> { columns.add(createColumn(column)); }); @@ -137,6 +139,27 @@ public static void setExactWidth(TableColumn column, double width) { column.setMaxWidth(width); } + /** + * Creates a column with the search score + */ + private TableColumn createScoreColumn(MainTableColumnModel columnModel) { + TableColumn column = new MainTableColumn<>(columnModel); + Node header = new Text(Localization.lang("Score")); + header.getStyleClass().add("mainTable-header"); + Tooltip.install(header, new Tooltip(MainTableColumnModel.Type.SCORE.getDisplayName())); + column.setGraphic(header); + column.setStyle("-fx-alignment: CENTER-RIGHT;"); + column.setCellValueFactory(cellData -> cellData.getValue().searchScoreProperty().asString("%.2f")); + new ValueTableCellFactory() + .withText(text -> text) + .install(column); + column.setSortable(true); + column.setSortType(TableColumn.SortType.DESCENDING); + column.visibleProperty().bind(stateManager.activeSearchQueryProperty().isPresent()); + column.setReorderable(false); + return column; + } + /** * Creates a column with a continous number */ @@ -239,6 +262,7 @@ private TableColumn> createFilesColumn( database, dialogService, preferencesService, + stateManager, taskExecutor); } @@ -250,6 +274,7 @@ private TableColumn> createExtraFileCol database, dialogService, preferencesService, + stateManager, columnModel.getQualifier(), taskExecutor); } diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java index 77bcecaf7a1..c5d2bcd7c94 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java @@ -32,6 +32,7 @@ public class MainTableColumnModel { private static final Logger LOGGER = LoggerFactory.getLogger(MainTableColumnModel.class); public enum Type { + SCORE("search_score", Localization.lang("Search score")), INDEX("index", Localization.lang("Index")), EXTRAFILE("extrafile", Localization.lang("File type")), FILES("files", Localization.lang("Linked files")), diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index bc91167bb88..da44ba88919 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -1,13 +1,12 @@ package org.jabref.gui.maintable; +import java.io.IOException; import java.util.List; import java.util.Optional; -import javafx.beans.binding.Bindings; -import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -17,72 +16,125 @@ import org.jabref.gui.groups.GroupsPreferences; import org.jabref.gui.util.BindingsHelper; import org.jabref.logic.search.SearchQuery; +import org.jabref.logic.search.retrieval.LuceneSearcher; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; +import org.jabref.model.groups.SearchGroup; import org.jabref.model.search.matchers.MatcherSet; import org.jabref.model.search.matchers.MatcherSets; +import org.jabref.model.search.rules.SearchRules; import org.jabref.preferences.PreferencesService; import com.tobiasdiez.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class MainTableDataModel { + private static final Logger LOGGER = LoggerFactory.getLogger(MainTableDataModel.class); + private final FilteredList entriesFiltered; private final SortedList entriesFilteredAndSorted; private final ObjectProperty fieldValueFormatter; private final GroupsPreferences groupsPreferences; private final NameDisplayPreferences nameDisplayPreferences; private final BibDatabaseContext bibDatabaseContext; + private final StateManager stateManager; + private Optional lastSearchQuery = Optional.empty(); public MainTableDataModel(BibDatabaseContext context, PreferencesService preferencesService, StateManager stateManager) { this.groupsPreferences = preferencesService.getGroupsPreferences(); this.nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); this.bibDatabaseContext = context; + this.stateManager = stateManager; this.fieldValueFormatter = new SimpleObjectProperty<>( new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); ObservableList allEntries = BindingsHelper.forUI(context.getDatabase().getEntries()); ObservableList entriesViewModel = EasyBind.mapBacked(allEntries, entry -> - new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter)); + new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter, stateManager)); entriesFiltered = new FilteredList<>(entriesViewModel); entriesFiltered.predicateProperty().bind( EasyBind.combine(stateManager.activeGroupProperty(), stateManager.activeSearchQueryProperty(), groupsPreferences.groupViewModeProperty(), - (groups, query, groupViewMode) -> entry -> isMatched(groups, query, entry)) + (groups, query, groupViewMode) -> { + // TODO btut: do not repeat search if display mode changes. Check if the same can be done for groups + doSearch(query); + return entry -> { + updateSearchGroups(stateManager, bibDatabaseContext); + return isMatched(groups, query, entry); + }; + }) ); + entriesViewModel.addListener((ListChangeListener) c -> { + if (stateManager.activeSearchQueryProperty().isPresent().get()) { + while (c.next()) { + if (c.wasAdded()) { + doSearch(stateManager.activeSearchQueryProperty().get()); + return; + } + } + } + }); - IntegerProperty resultSize = new SimpleIntegerProperty(); - resultSize.bind(Bindings.size(entriesFiltered)); - stateManager.setActiveSearchResultSize(context, resultSize); // We need to wrap the list since otherwise sorting in the table does not work entriesFilteredAndSorted = new SortedList<>(entriesFiltered); } + public void removeBindings() { + entriesFiltered.predicateProperty().unbind(); + } + + public static void updateSearchGroups(StateManager stateManager, BibDatabaseContext bibDatabaseContext) { + stateManager.getSelectedGroups(bibDatabaseContext).stream().map(GroupTreeNode::getGroup).filter(g -> g instanceof SearchGroup).map(g -> ((SearchGroup) g)).forEach(g -> g.updateMatches(bibDatabaseContext)); + } + + private void doSearch(Optional query) { + if (lastSearchQuery.isPresent() && lastSearchQuery.equals(query)) { + return; + } + lastSearchQuery = query; + stateManager.getSearchResults().remove(bibDatabaseContext); + if (query.isPresent() && query.get().toString().length() > 0) { + try { + // TODO btut: maybe do in background? + stateManager.getSearchResults().put(bibDatabaseContext, LuceneSearcher.of(bibDatabaseContext).search(query.get())); + } catch (IOException e) { + LOGGER.debug("Failed to run database search '{}'", query.get(), e); + } + } + } + private boolean isMatched(ObservableList groups, Optional query, BibEntryTableViewModel entry) { return isMatchedByGroup(groups, entry) && isMatchedBySearch(query, entry); } private boolean isMatchedBySearch(Optional query, BibEntryTableViewModel entry) { - return query.map(matcher -> matcher.isMatch(entry.getEntry())) - .orElse(true); + if (query.isEmpty() || !query.get().getSearchFlags().contains(SearchRules.SearchFlags.FILTERING_SEARCH)) { + return true; + } + return entry.getSearchScore() > 0; } private boolean isMatchedByGroup(ObservableList groups, BibEntryTableViewModel entry) { - return createGroupMatcher(groups) - .map(matcher -> matcher.isMatch(entry.getEntry())) + if (!groupsPreferences.groupViewModeProperty().contains(GroupViewMode.FILTER)) { + return true; + } + return createGroupMatcher(groups, groupsPreferences) + .map(matcher -> groupsPreferences.getGroupViewMode().contains(GroupViewMode.INVERT) ^ matcher.isMatch(entry.getEntry())) .orElse(true); } - private Optional createGroupMatcher(List selectedGroups) { + public static Optional createGroupMatcher(List selectedGroups, GroupsPreferences groupsPreferences) { if ((selectedGroups == null) || selectedGroups.isEmpty()) { // No selected group, show all entries return Optional.empty(); } final MatcherSet searchRules = MatcherSets.build( - groupsPreferences.getGroupViewMode() == GroupViewMode.INTERSECTION + groupsPreferences.getGroupViewMode().contains(GroupViewMode.INTERSECTION) ? MatcherSets.MatcherType.AND : MatcherSets.MatcherType.OR); diff --git a/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java b/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java index 6dd1634c1a5..130ad126c8b 100644 --- a/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java +++ b/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java @@ -11,6 +11,7 @@ import javafx.scene.input.MouseButton; import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.fieldeditors.LinkedFileViewModel; @@ -35,6 +36,7 @@ public class FileColumn extends MainTableColumn> { private final DialogService dialogService; private final BibDatabaseContext database; private final PreferencesService preferencesService; + private final StateManager stateManager; private final TaskExecutor taskExecutor; /** @@ -44,11 +46,13 @@ public FileColumn(MainTableColumnModel model, BibDatabaseContext database, DialogService dialogService, PreferencesService preferencesService, + StateManager stateManager, TaskExecutor taskExecutor) { super(model); this.database = Objects.requireNonNull(database); this.dialogService = dialogService; this.preferencesService = preferencesService; + this.stateManager = stateManager; this.taskExecutor = taskExecutor; setCommonSettings(); @@ -83,12 +87,14 @@ public FileColumn(MainTableColumnModel model, BibDatabaseContext database, DialogService dialogService, PreferencesService preferencesService, + StateManager stateManager, String fileType, TaskExecutor taskExecutor) { super(model); this.database = Objects.requireNonNull(database); this.dialogService = dialogService; this.preferencesService = preferencesService; + this.stateManager = stateManager; this.taskExecutor = taskExecutor; setCommonSettings(); @@ -98,7 +104,7 @@ public FileColumn(MainTableColumnModel model, .getGraphicNode()); new ValueTableCellFactory>() - .withGraphic(linkedFiles -> createFileIcon(linkedFiles.stream().filter(linkedFile -> + .withGraphic((entry, linkedFiles) -> createFileIcon(entry, linkedFiles.stream().filter(linkedFile -> linkedFile.getFileType().equalsIgnoreCase(fileType)).collect(Collectors.toList()))) .install(this); } @@ -141,7 +147,10 @@ private ContextMenu createFileMenu(BibEntryTableViewModel entry, List linkedFiles) { + private Node createFileIcon(BibEntryTableViewModel entry, List linkedFiles) { + if (linkedFiles.size() > 0 && stateManager.getSearchResults().containsKey(database) && stateManager.getSearchResults().get(database).containsKey(entry.getEntry()) && stateManager.getSearchResults().get(database).get(entry.getEntry()).hasFulltextResults()) { + return IconTheme.JabRefIcons.FILE_SEARCH.getGraphicNode(); + } if (linkedFiles.size() > 1) { return IconTheme.JabRefIcons.FILE_MULTIPLE.getGraphicNode(); } else if (linkedFiles.size() == 1) { diff --git a/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java b/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java index 84286ab62f3..0d1d095108f 100644 --- a/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java @@ -22,15 +22,12 @@ public GroupsTabViewModel(GroupsPreferences groupsPreferences) { @Override public void setValues() { - switch (groupsPreferences.getGroupViewMode()) { - case INTERSECTION -> { - groupViewModeIntersectionProperty.setValue(true); - groupViewModeUnionProperty.setValue(false); - } - case UNION -> { - groupViewModeIntersectionProperty.setValue(false); - groupViewModeUnionProperty.setValue(true); - } + if (groupsPreferences.getGroupViewMode().contains(GroupViewMode.INTERSECTION)) { + groupViewModeIntersectionProperty.setValue(true); + groupViewModeUnionProperty.setValue(false); + } else { + groupViewModeIntersectionProperty.setValue(false); + groupViewModeUnionProperty.setValue(true); } autoAssignGroupProperty.setValue(groupsPreferences.shouldAutoAssignGroup()); displayGroupCountProperty.setValue(groupsPreferences.shouldDisplayGroupCount()); @@ -38,7 +35,7 @@ public void setValues() { @Override public void storeSettings() { - groupsPreferences.setGroupViewMode(groupViewModeIntersectionProperty.getValue() ? GroupViewMode.INTERSECTION : GroupViewMode.UNION); + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, groupViewModeIntersectionProperty.getValue()); groupsPreferences.setAutoAssignGroup(autoAssignGroupProperty.getValue()); groupsPreferences.setDisplayGroupCount(displayGroupCountProperty.getValue()); } diff --git a/src/main/java/org/jabref/gui/preview/PreviewPanel.java b/src/main/java/org/jabref/gui/preview/PreviewPanel.java index a2416d43098..e98371875c6 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewPanel.java +++ b/src/main/java/org/jabref/gui/preview/PreviewPanel.java @@ -25,8 +25,8 @@ 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.preview.PreviewLayout; +import org.jabref.logic.search.indexing.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreferencesService; @@ -44,8 +44,6 @@ public class PreviewPanel extends VBox { private final PreviewViewer previewView; private final PreviewPreferences previewPreferences; private final DialogService dialogService; - private final StateManager stateManager; - private final IndexingTaskManager indexingTaskManager; private BibEntry entry; public PreviewPanel(BibDatabaseContext database, @@ -58,10 +56,8 @@ public PreviewPanel(BibDatabaseContext database, TaskExecutor taskExecutor) { this.keyBindingRepository = keyBindingRepository; this.dialogService = dialogService; - this.stateManager = stateManager; this.previewPreferences = preferencesService.getPreviewPreferences(); - this.indexingTaskManager = indexingTaskManager; - this.fileLinker = new ExternalFilesEntryLinker(preferencesService.getFilePreferences(), database, dialogService); + this.fileLinker = new ExternalFilesEntryLinker(preferencesService, preferencesService.getFilePreferences(), database, dialogService); PreviewPreferences previewPreferences = preferencesService.getPreviewPreferences(); previewView = new PreviewViewer(database, dialogService, preferencesService, stateManager, themeManager, taskExecutor); diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 544275f741a..6ef8e38cf5b 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -136,7 +136,7 @@ function getSelectionHtml() { private boolean registered; private final ChangeListener> listener = (queryObservable, queryOldValue, queryNewValue) -> { - searchHighlightPattern = queryNewValue.flatMap(SearchQuery::getJavaScriptPatternForWords); + // searchHighlightPattern = queryNewValue.flatMap(SearchQuery::getJavaScriptPatternForWords); TODO btut: Pattern-Highlighting with lucene highlightSearchPattern(); }; diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 34ac62b9de0..8d820a714f8 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -2,7 +2,9 @@ import java.lang.reflect.Field; import java.time.Duration; +import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; @@ -16,6 +18,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.css.PseudoClass; import javafx.event.Event; @@ -53,7 +56,6 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; -import org.jabref.gui.search.rules.describer.SearchDescribers; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.BindingsHelper; import org.jabref.gui.util.DefaultTaskExecutor; @@ -61,7 +63,10 @@ import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; import org.jabref.logic.search.SearchQuery; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.Author; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.pdf.search.LuceneSearchResults; import org.jabref.model.search.rules.SearchRules; import org.jabref.preferences.PreferencesService; import org.jabref.preferences.SearchPreferences; @@ -89,12 +94,12 @@ public class GlobalSearchBar extends HBox { private static final PseudoClass CLASS_RESULTS_FOUND = PseudoClass.getPseudoClass("emptyResult"); private final CustomTextField searchField = SearchTextField.create(); - private final ToggleButton caseSensitiveButton; private final ToggleButton regularExpressionButton; private final ToggleButton fulltextButton; private final Button openGlobalSearchButton; private final ToggleButton keepSearchString; - // private final Button searchModeButton; + private final ToggleButton filterModeButton; + private final ToggleButton sortByScoreButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); @@ -158,31 +163,35 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences ClipBoardManager.addX11Support(searchField); regularExpressionButton = IconTheme.JabRefIcons.REG_EX.asToggleButton(); - caseSensitiveButton = IconTheme.JabRefIcons.CASE_SENSITIVE.asToggleButton(); fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); openGlobalSearchButton = IconTheme.JabRefIcons.OPEN_GLOBAL_SEARCH.asButton(); keepSearchString = IconTheme.JabRefIcons.KEEP_SEARCH_STRING.asToggleButton(); + filterModeButton = IconTheme.JabRefIcons.FILTER.asToggleButton(); + sortByScoreButton = IconTheme.JabRefIcons.SORT_BY_SCORE.asToggleButton(); initSearchModifierButtons(); BooleanBinding focusedOrActive = searchField.focusedProperty() .or(regularExpressionButton.focusedProperty()) - .or(caseSensitiveButton.focusedProperty()) .or(fulltextButton.focusedProperty()) .or(keepSearchString.focusedProperty()) + .or(filterModeButton.focusedProperty()) + .or(sortByScoreButton.focusedProperty()) .or(searchField.textProperty() .isNotEmpty()); regularExpressionButton.visibleProperty().unbind(); regularExpressionButton.visibleProperty().bind(focusedOrActive); - caseSensitiveButton.visibleProperty().unbind(); - caseSensitiveButton.visibleProperty().bind(focusedOrActive); fulltextButton.visibleProperty().unbind(); fulltextButton.visibleProperty().bind(focusedOrActive); keepSearchString.visibleProperty().unbind(); keepSearchString.visibleProperty().bind(focusedOrActive); + filterModeButton.visibleProperty().unbind(); + filterModeButton.visibleProperty().bind(focusedOrActive); + sortByScoreButton.visibleProperty().unbind(); + sortByScoreButton.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); + StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, fulltextButton, keepSearchString, filterModeButton, sortByScoreButton)); modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); @@ -209,10 +218,11 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences // Async update searchTask.restart(); }, - query -> setSearchTerm(query.map(SearchQuery::getQuery).orElse(""))); + query -> setSearchTerm(query.orElse(new SearchQuery("", EnumSet.noneOf(SearchRules.SearchFlags.class))))); - this.stateManager.activeSearchQueryProperty().addListener((obs, oldvalue, newValue) -> newValue.ifPresent(this::updateSearchResultsForQuery)); - this.stateManager.activeDatabaseProperty().addListener((obs, oldValue, newValue) -> stateManager.activeSearchQueryProperty().get().ifPresent(this::updateSearchResultsForQuery)); + this.stateManager.getSearchResults().addListener((MapChangeListener>) change -> { + stateManager.activeSearchQueryProperty().get().ifPresent(this::updateSearchResultsForQuery); + }); /* * The listener tracks a change on the focus property value. * This happens, from active (user types a query) to inactive / focus @@ -228,8 +238,7 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences } private void updateSearchResultsForQuery(SearchQuery query) { - updateResults(this.stateManager.getSearchResultSize().intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), - query.isGrammarBasedSearch()); + updateResults(this.stateManager.getSearchResults().values().stream().map(Map::size).reduce(0, Integer::sum)); } private void initSearchModifierButtons() { @@ -241,14 +250,6 @@ private void initSearchModifierButtons() { performSearch(); }); - caseSensitiveButton.setSelected(searchPreferences.isCaseSensitive()); - caseSensitiveButton.setTooltip(new Tooltip(Localization.lang("Case sensitive"))); - initSearchModifierButton(caseSensitiveButton); - caseSensitiveButton.setOnAction(event -> { - searchPreferences.setSearchFlag(SearchRules.SearchFlags.CASE_SENSITIVE, caseSensitiveButton.isSelected()); - performSearch(); - }); - fulltextButton.setSelected(searchPreferences.isFulltext()); fulltextButton.setTooltip(new Tooltip(Localization.lang("Fulltext search"))); initSearchModifierButton(fulltextButton); @@ -265,6 +266,22 @@ private void initSearchModifierButtons() { performSearch(); }); + filterModeButton.setSelected(searchPreferences.isFilteringMode()); + filterModeButton.setTooltip(new Tooltip(Localization.lang("Filter search results"))); + initSearchModifierButton(filterModeButton); + filterModeButton.setOnAction(event -> { + searchPreferences.setSearchFlag(SearchRules.SearchFlags.FILTERING_SEARCH, filterModeButton.isSelected()); + performSearch(); + }); + + sortByScoreButton.setSelected(searchPreferences.isSortByScore()); + sortByScoreButton.setTooltip(new Tooltip(Localization.lang("Always sort by score"))); + initSearchModifierButton(sortByScoreButton); + sortByScoreButton.setOnAction(event -> { + searchPreferences.setSearchFlag(SearchRules.SearchFlags.SORT_BY_SCORE, sortByScoreButton.isSelected()); + performSearch(); + }); + openGlobalSearchButton.disableProperty().bindBidirectional(globalSearchActive); openGlobalSearchButton.setTooltip(new Tooltip(Localization.lang("Search across libraries in a new window"))); initSearchModifierButton(openGlobalSearchButton); @@ -370,7 +387,7 @@ private AutoCompletePopup getPopup(AutoCompletionBinding autoCompletio } } - private void updateResults(int matched, TextFlow description, boolean grammarBasedSearch) { + private void updateResults(int matched) { if (matched == 0) { currentResults.setText(Localization.lang("No results found.")); searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); @@ -378,20 +395,13 @@ private void updateResults(int matched, TextFlow description, boolean grammarBas currentResults.setText(Localization.lang("Found %0 results.", String.valueOf(matched))); searchField.pseudoClassStateChanged(CLASS_RESULTS_FOUND, true); } - if (grammarBasedSearch) { - // TODO: switch Icon color - // searchIcon.setIcon(IconTheme.JabRefIcon.ADVANCED_SEARCH.getIcon()); - } else { - // TODO: switch Icon color - // searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getIcon()); - } - setSearchFieldHintTooltip(description); + // setSearchFieldHintTooltip(description); TODO btut: Search-tooltip for lucene } private void setSearchFieldHintTooltip(TextFlow description) { if (preferencesService.getWorkspacePreferences().shouldShowAdvancedHints()) { - String genericDescription = Localization.lang("Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor=Smith and title=electrical"); + String genericDescription = Localization.lang("Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor:Smith AND title:electrical"); List genericDescriptionTexts = TooltipTextUtil.createTextsFromHtml(genericDescription); if (description == null) { @@ -410,12 +420,12 @@ public void updateHintVisibility() { setSearchFieldHintTooltip(null); } - public void setSearchTerm(String searchTerm) { - if (searchTerm.equals(searchField.getText())) { + public void setSearchTerm(SearchQuery searchQuery) { + if (searchQuery.toString().equals(searchField.getText())) { return; } - DefaultTaskExecutor.runInJavaFXThread(() -> searchField.setText(searchTerm)); + DefaultTaskExecutor.runInJavaFXThread(() -> searchField.setText(searchQuery.toString())); } private static class SearchPopupSkin implements Skin> { diff --git a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java index 4fb2a575ab6..b96580e0565 100644 --- a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java +++ b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java @@ -9,9 +9,10 @@ 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.search.indexing.LuceneIndexer; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +26,7 @@ public class RebuildFulltextSearchIndexAction extends SimpleCommand { private final StateManager stateManager; private final GetCurrentLibraryTab currentLibraryTab; private final DialogService dialogService; + private final PreferencesService preferencesService; private final FilePreferences filePreferences; private final TaskExecutor taskExecutor; @@ -32,14 +34,12 @@ public class RebuildFulltextSearchIndexAction extends SimpleCommand { private boolean shouldContinue = true; - public RebuildFulltextSearchIndexAction(StateManager stateManager, - GetCurrentLibraryTab currentLibraryTab, - DialogService dialogService, - FilePreferences filePreferences, + public RebuildFulltextSearchIndexAction(StateManager stateManager, GetCurrentLibraryTab currentLibraryTab, DialogService dialogService, PreferencesService preferences, FilePreferences filePreferences, TaskExecutor taskExecutor) { this.stateManager = stateManager; this.currentLibraryTab = currentLibraryTab; this.dialogService = dialogService; + this.preferencesService = preferences; this.filePreferences = filePreferences; this.taskExecutor = taskExecutor; @@ -74,8 +74,8 @@ private void rebuildIndex() { return; } try { - currentLibraryTab.get().getIndexingTaskManager().createIndex(PdfIndexer.of(databaseContext, filePreferences)); - currentLibraryTab.get().getIndexingTaskManager().updateIndex(PdfIndexer.of(databaseContext, filePreferences), databaseContext); + currentLibraryTab.get().getIndexingTaskManager().createIndex(LuceneIndexer.of(databaseContext, preferencesService)); + currentLibraryTab.get().getIndexingTaskManager().updateIndex(LuceneIndexer.of(databaseContext, preferencesService)); } 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/gui/search/SearchDisplayMode.java b/src/main/java/org/jabref/gui/search/SearchDisplayMode.java deleted file mode 100644 index 18074f57a71..00000000000 --- a/src/main/java/org/jabref/gui/search/SearchDisplayMode.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.jabref.gui.search; - -import java.util.function.Supplier; - -import org.jabref.logic.l10n.Localization; - -/** - * Collects the possible search modes - */ -public enum SearchDisplayMode { - - FLOAT(() -> Localization.lang("Float"), () -> Localization.lang("Gray out non-hits")), - FILTER(() -> Localization.lang("Filter"), () -> Localization.lang("Hide non-hits")); - - private final Supplier displayName; - private final Supplier toolTipText; - - /** - * We have to use supplier for the localized text so that language changes are correctly reflected. - */ - SearchDisplayMode(Supplier displayName, Supplier toolTipText) { - this.displayName = displayName; - this.toolTipText = toolTipText; - } - - public String getDisplayName() { - return displayName.get(); - } - - public String getToolTipText() { - return toolTipText.get(); - } -} diff --git a/src/main/java/org/jabref/gui/search/SearchResultsTable.java b/src/main/java/org/jabref/gui/search/SearchResultsTable.java index 2e4a3c710f5..49979257068 100644 --- a/src/main/java/org/jabref/gui/search/SearchResultsTable.java +++ b/src/main/java/org/jabref/gui/search/SearchResultsTable.java @@ -4,6 +4,7 @@ import javax.swing.undo.UndoManager; +import javafx.collections.ListChangeListener; import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -13,6 +14,7 @@ import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTable; import org.jabref.gui.maintable.MainTableColumnFactory; +import org.jabref.gui.maintable.MainTableColumnModel; import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.maintable.PersistenceVisualStateTable; import org.jabref.gui.maintable.SmartConstrainedResizePolicy; @@ -49,6 +51,22 @@ public SearchResultsTable(SearchResultsTableDataModel model, } this.getColumns().addAll(allCols); + TableColumn scoreColumn = this.getColumns().stream().filter(c -> c instanceof MainTableColumn) + .map(c -> ((MainTableColumn) c)) + .filter(c -> c.getModel().getType() == MainTableColumnModel.Type.SCORE) + .findFirst().orElse(null); + + // always sort by score first. If no search is ongoing, it will be equal for all columns. + ListChangeListener> scoreSortOderPrioritizer = new ListChangeListener>() { + @Override + public void onChanged(Change> c) { + getSortOrder().removeListener(this); + getSortOrder().removeAll(scoreColumn); + getSortOrder().add(0, scoreColumn); + getSortOrder().addListener(this); + } + }; + this.getSortOrder().clear(); preferencesService.getSearchDialogColumnPreferences().getColumnSortOrder().forEach(columnModel -> this.getColumns().stream() @@ -56,6 +74,11 @@ public SearchResultsTable(SearchResultsTableDataModel model, .filter(column -> column.getModel().equals(columnModel)) .findFirst() .ifPresent(column -> this.getSortOrder().add(column))); + // insert score sort order + if (scoreColumn != null) { + this.getSortOrder().add(0, scoreColumn); + this.getSortOrder().addListener(scoreSortOderPrioritizer); + } if (mainTablePreferences.getResizeColumnsToFit()) { this.setColumnResizePolicy(new SmartConstrainedResizePolicy()); @@ -65,6 +88,7 @@ public SearchResultsTable(SearchResultsTableDataModel model, this.setItems(model.getEntriesFilteredAndSorted()); // Enable sorting model.getEntriesFilteredAndSorted().comparatorProperty().bind(this.comparatorProperty()); + this.sort(); this.getStylesheets().add(MainTable.class.getResource("MainTable.css").toExternalForm()); diff --git a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java index 967c397b799..c3bf43e9873 100644 --- a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java +++ b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java @@ -1,7 +1,6 @@ package org.jabref.gui.search; import java.util.List; -import java.util.Optional; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -14,7 +13,6 @@ import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTableFieldValueFormatter; import org.jabref.gui.maintable.NameDisplayPreferences; -import org.jabref.logic.search.SearchQuery; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreferencesService; @@ -37,22 +35,17 @@ public SearchResultsTableDataModel(BibDatabaseContext bibDatabaseContext, Prefer ObservableList entriesViewModel = FXCollections.observableArrayList(); for (BibDatabaseContext context : stateManager.getOpenDatabases()) { ObservableList entriesForDb = context.getDatabase().getEntries(); - List viewModelForDb = EasyBind.mapBacked(entriesForDb, entry -> new BibEntryTableViewModel(entry, context, fieldValueFormatter)); + List viewModelForDb = EasyBind.mapBacked(entriesForDb, entry -> new BibEntryTableViewModel(entry, context, fieldValueFormatter, stateManager)); entriesViewModel.addAll(viewModelForDb); } entriesFiltered = new FilteredList<>(entriesViewModel); - entriesFiltered.predicateProperty().bind(EasyBind.map(stateManager.activeSearchQueryProperty(), query -> entry -> isMatchedBySearch(query, entry))); + entriesFiltered.setPredicate(entry -> entry.getSearchScore() > 0); // We need to wrap the list since otherwise sorting in the table does not work entriesSorted = new SortedList<>(entriesFiltered); } - private boolean isMatchedBySearch(Optional query, BibEntryTableViewModel entry) { - return query.map(matcher -> matcher.isMatch(entry.getEntry())) - .orElse(true); - } - public SortedList getEntriesFilteredAndSorted() { return entriesSorted; } diff --git a/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java deleted file mode 100644 index d411c263c7c..00000000000 --- a/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jabref.gui.search.rules.describer; - -import java.util.EnumSet; -import java.util.List; - -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; - -import org.jabref.gui.util.TooltipTextUtil; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.model.search.rules.SentenceAnalyzer; - -public class ContainsAndRegexBasedSearchRuleDescriber implements SearchDescriber { - - private final EnumSet searchFlags; - private final String query; - - public ContainsAndRegexBasedSearchRuleDescriber(EnumSet searchFlags, String query) { - this.searchFlags = searchFlags; - this.query = query; - } - - @Override - public TextFlow getDescription() { - List words = new SentenceAnalyzer(query).getWords(); - String firstWord = words.isEmpty() ? "" : words.get(0); - - String temp = searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? Localization.lang( - "This search contains entries in which any field contains the regular expression %0") - : Localization.lang("This search contains entries in which any field contains the term %0"); - List textList = TooltipTextUtil.formatToTexts(temp, new TooltipTextUtil.TextReplacement("%0", firstWord, TooltipTextUtil.TextType.BOLD)); - - if (words.size() > 1) { - List unprocessedWords = words.subList(1, words.size()); - for (String word : unprocessedWords) { - textList.add(TooltipTextUtil.createText(String.format(" %s ", Localization.lang("and")), TooltipTextUtil.TextType.NORMAL)); - textList.add(TooltipTextUtil.createText(word, TooltipTextUtil.TextType.BOLD)); - } - } - - textList.add(getCaseSensitiveDescription()); - - TextFlow searchDescription = new TextFlow(); - searchDescription.getChildren().setAll(textList); - return searchDescription; - } - - private Text getCaseSensitiveDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return TooltipTextUtil.createText(String.format(" (%s). ", Localization.lang("case sensitive")), TooltipTextUtil.TextType.NORMAL); - } else { - return TooltipTextUtil.createText(String.format(" (%s). ", Localization.lang("case insensitive")), TooltipTextUtil.TextType.NORMAL); - } - } -} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java deleted file mode 100644 index 9ca6bea4e94..00000000000 --- a/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.jabref.gui.search.rules.describer; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.regex.Pattern; - -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; - -import org.jabref.gui.util.TooltipTextUtil; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.search.rules.GrammarBasedSearchRule; -import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.model.strings.StringUtil; -import org.jabref.search.SearchBaseVisitor; -import org.jabref.search.SearchParser; - -import org.antlr.v4.runtime.tree.ParseTree; - -public class GrammarBasedSearchRuleDescriber implements SearchDescriber { - - private final EnumSet searchFlags; - private final ParseTree parseTree; - - public GrammarBasedSearchRuleDescriber(EnumSet searchFlags, ParseTree parseTree) { - this.searchFlags = searchFlags; - this.parseTree = Objects.requireNonNull(parseTree); - } - - @Override - public TextFlow getDescription() { - TextFlow textFlow = new TextFlow(); - DescriptionSearchBaseVisitor descriptionSearchBaseVisitor = new DescriptionSearchBaseVisitor(); - - // describe advanced search expression - textFlow.getChildren().add(TooltipTextUtil.createText(String.format("%s ", Localization.lang("This search contains entries in which")), TooltipTextUtil.TextType.NORMAL)); - textFlow.getChildren().addAll(descriptionSearchBaseVisitor.visit(parseTree)); - textFlow.getChildren().add(TooltipTextUtil.createText(". ", TooltipTextUtil.TextType.NORMAL)); - textFlow.getChildren().add(TooltipTextUtil.createText(searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? Localization - .lang("The search is case-sensitive.") : - Localization.lang("The search is case-insensitive."), TooltipTextUtil.TextType.NORMAL)); - return textFlow; - } - - private class DescriptionSearchBaseVisitor extends SearchBaseVisitor> { - - @Override - public List visitStart(SearchParser.StartContext context) { - return visit(context.expression()); - } - - @Override - public List visitUnaryExpression(SearchParser.UnaryExpressionContext context) { - List textList = visit(context.expression()); - textList.add(0, TooltipTextUtil.createText(Localization.lang("not").concat(" "), TooltipTextUtil.TextType.NORMAL)); - return textList; - } - - @Override - public List visitParenExpression(SearchParser.ParenExpressionContext context) { - ArrayList textList = new ArrayList<>(); - textList.add(TooltipTextUtil.createText(String.format("%s", context.expression()), TooltipTextUtil.TextType.NORMAL)); - return textList; - } - - @Override - public List visitBinaryExpression(SearchParser.BinaryExpressionContext context) { - List textList = visit(context.left); - if ("AND".equalsIgnoreCase(context.operator.getText())) { - textList.add(TooltipTextUtil.createText(String.format(" %s ", Localization.lang("and")), TooltipTextUtil.TextType.NORMAL)); - } else { - textList.add(TooltipTextUtil.createText(String.format(" %s ", Localization.lang("or")), TooltipTextUtil.TextType.NORMAL)); - } - textList.addAll(visit(context.right)); - return textList; - } - - @Override - public List visitComparison(SearchParser.ComparisonContext context) { - final List textList = new ArrayList<>(); - final Optional fieldDescriptor = Optional.ofNullable(context.left); - final String value = StringUtil.unquote(context.right.getText(), '"'); - if (!fieldDescriptor.isPresent()) { - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(searchFlags, value).getDescription(); - description.getChildren().forEach(it -> textList.add((Text) it)); - return textList; - } - - final String field = StringUtil.unquote(fieldDescriptor.get().getText(), '"'); - final GrammarBasedSearchRule.ComparisonOperator operator = GrammarBasedSearchRule.ComparisonOperator.build(context.operator.getText()); - - final boolean regExpFieldSpec = !Pattern.matches("\\w+", field); - String temp = regExpFieldSpec ? Localization.lang( - "any field that matches the regular expression %0") : Localization.lang("the field %0"); - - if (operator == GrammarBasedSearchRule.ComparisonOperator.CONTAINS) { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - temp = Localization.lang("%0 contains the regular expression %1", temp); - } else { - temp = Localization.lang("%0 contains the term %1", temp); - } - } else if (operator == GrammarBasedSearchRule.ComparisonOperator.EXACT) { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - temp = Localization.lang("%0 matches the regular expression %1", temp); - } else { - temp = Localization.lang("%0 matches the term %1", temp); - } - } else if (operator == GrammarBasedSearchRule.ComparisonOperator.DOES_NOT_CONTAIN) { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - temp = Localization.lang("%0 doesn't contain the regular expression %1", temp); - } else { - temp = Localization.lang("%0 doesn't contain the term %1", temp); - } - } else { - throw new IllegalStateException("CANNOT HAPPEN!"); - } - - List formattedTexts = TooltipTextUtil.formatToTexts(temp, - new TooltipTextUtil.TextReplacement("%0", field, TooltipTextUtil.TextType.BOLD), - new TooltipTextUtil.TextReplacement("%1", value, TooltipTextUtil.TextType.BOLD)); - textList.addAll(formattedTexts); - return textList; - } - } -} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescriber.java deleted file mode 100644 index d3a18ecfd60..00000000000 --- a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescriber.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.jabref.gui.search.rules.describer; - -import javafx.scene.text.TextFlow; - -@FunctionalInterface -public interface SearchDescriber { - - TextFlow getDescription(); -} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java deleted file mode 100644 index de696f03632..00000000000 --- a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.jabref.gui.search.rules.describer; - -import org.jabref.logic.search.SearchQuery; -import org.jabref.model.search.rules.ContainsBasedSearchRule; -import org.jabref.model.search.rules.GrammarBasedSearchRule; -import org.jabref.model.search.rules.RegexBasedSearchRule; - -public class SearchDescribers { - - private SearchDescribers() { - } - - /** - * Get the search describer for a given search query. - * - * @param searchQuery the search query - * @return the search describer to turn the search into something human understandable - */ - public static SearchDescriber getSearchDescriberFor(SearchQuery searchQuery) { - if (searchQuery.getRule() instanceof GrammarBasedSearchRule grammarBasedSearchRule) { - return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.getSearchFlags(), grammarBasedSearchRule.getTree()); - } else if (searchQuery.getRule() instanceof ContainsBasedSearchRule containBasedSearchRule) { - return new ContainsAndRegexBasedSearchRuleDescriber(containBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); - } else if (searchQuery.getRule() instanceof RegexBasedSearchRule regexBasedSearchRule) { - return new ContainsAndRegexBasedSearchRuleDescriber(regexBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); - } else { - throw new IllegalStateException("Cannot find a describer for searchRule " + searchQuery.getRule() + " and query " + searchQuery.getQuery()); - } - } -} diff --git a/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java index 1ead8207f46..bbec37ec5e0 100644 --- a/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java +++ b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java @@ -1,6 +1,11 @@ package org.jabref.gui.sidepane; +import java.util.EnumSet; + +import javafx.collections.SetChangeListener; import javafx.scene.control.Button; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; import org.jabref.gui.DialogService; import org.jabref.gui.actions.SimpleCommand; @@ -10,12 +15,12 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.logic.l10n.Localization; -import com.tobiasdiez.easybind.EasyBind; - public class GroupsSidePaneComponent extends SidePaneComponent { private final GroupsPreferences groupsPreferences; private final DialogService dialogService; private final Button intersectionUnionToggle = IconTheme.JabRefIcons.GROUP_INTERSECTION.asButton(); + private final ToggleButton filterToggle = IconTheme.JabRefIcons.FILTER.asToggleButton(); + private final ToggleButton invertToggle = IconTheme.JabRefIcons.INVERT.asToggleButton(); public GroupsSidePaneComponent(SimpleCommand closeCommand, SimpleCommand moveUpCommand, @@ -26,31 +31,47 @@ public GroupsSidePaneComponent(SimpleCommand closeCommand, super(SidePaneType.GROUPS, closeCommand, moveUpCommand, moveDownCommand, contentFactory); this.groupsPreferences = groupsPreferences; this.dialogService = dialogService; + setupInvertToggle(); + setupFilterToggle(); setupIntersectionUnionToggle(); - EasyBind.subscribe(groupsPreferences.groupViewModeProperty(), mode -> { - GroupModeViewModel modeViewModel = new GroupModeViewModel(mode); + groupsPreferences.groupViewModeProperty().addListener((SetChangeListener) change -> { + GroupModeViewModel modeViewModel = new GroupModeViewModel(groupsPreferences.groupViewModeProperty()); intersectionUnionToggle.setGraphic(modeViewModel.getUnionIntersectionGraphic()); intersectionUnionToggle.setTooltip(modeViewModel.getUnionIntersectionTooltip()); }); } private void setupIntersectionUnionToggle() { - addExtraButtonToHeader(intersectionUnionToggle, 0); + addExtraNodeToHeader(intersectionUnionToggle, 0); intersectionUnionToggle.setOnAction(event -> new ToggleUnionIntersectionAction().execute()); } + private void setupFilterToggle() { + addExtraNodeToHeader(filterToggle, 0); + filterToggle.setSelected(groupsPreferences.groupViewModeProperty().contains(GroupViewMode.FILTER)); + filterToggle.selectedProperty().addListener((observable, oldValue, newValue) -> groupsPreferences.setGroupViewMode(GroupViewMode.FILTER, newValue)); + filterToggle.setTooltip(new Tooltip(Localization.lang("Filter by groups"))); + } + + private void setupInvertToggle() { + addExtraNodeToHeader(invertToggle, 0); + invertToggle.setSelected(groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INVERT)); + invertToggle.selectedProperty().addListener((observable, oldValue, newValue) -> groupsPreferences.setGroupViewMode(GroupViewMode.INVERT, newValue)); + invertToggle.setTooltip(new Tooltip(Localization.lang("Invert groups"))); + } + private class ToggleUnionIntersectionAction extends SimpleCommand { @Override public void execute() { - GroupViewMode mode = groupsPreferences.getGroupViewMode(); + EnumSet mode = groupsPreferences.getGroupViewMode(); - if (mode == GroupViewMode.UNION) { - groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION); + if (mode.contains(GroupViewMode.INTERSECTION)) { + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, false); dialogService.notify(Localization.lang("Group view mode set to intersection")); - } else if (mode == GroupViewMode.INTERSECTION) { - groupsPreferences.setGroupViewMode(GroupViewMode.UNION); + } else { + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, true); dialogService.notify(Localization.lang("Group view mode set to union")); } } diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java index 8abb96582dc..6260a908123 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java @@ -69,7 +69,7 @@ private Node createHeaderView() { return headerView; } - protected void addExtraButtonToHeader(Button button, int position) { + protected void addExtraNodeToHeader(Node button, int position) { this.buttonContainer.getChildren().add(position, button); } diff --git a/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java b/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java index 87114e3b6a0..bbb25924408 100644 --- a/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java +++ b/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java @@ -1,8 +1,14 @@ package org.jabref.gui.util; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Function; +import javafx.beans.value.ObservableValue; +import javafx.css.PseudoClass; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.control.ContextMenu; @@ -19,6 +25,7 @@ import org.jabref.model.strings.StringUtil; +import com.tobiasdiez.easybind.Subscription; import org.reactfx.util.TriConsumer; /** @@ -37,6 +44,7 @@ public class ViewModelTableRowFactory implements Callback, Table private TriConsumer, S, ? super DragEvent> toOnDragOver; private TriConsumer, S, ? super MouseDragEvent> toOnMouseDragEntered; private Callback toTooltip; + private final Map>> pseudoClasses = new HashMap<>(); public ViewModelTableRowFactory withOnMouseClickedEvent(BiConsumer onMouseClickedEvent) { this.onMouseClickedEvent = onMouseClickedEvent; @@ -104,6 +112,11 @@ public ViewModelTableRowFactory withTooltip(Callback toTooltip) { return this; } + public ViewModelTableRowFactory withPseudoClass(PseudoClass pseudoClass, Callback> toCondition) { + this.pseudoClasses.putIfAbsent(pseudoClass, toCondition); + return this; + } + @Override public TableRow call(TableView tableView) { TableRow row = new TableRow<>(); @@ -194,6 +207,21 @@ public TableRow call(TableView tableView) { } }); } + + final List subscriptions = new ArrayList<>(); + row.itemProperty().addListener((observable, oldValue, newValue) -> { + subscriptions.forEach(Subscription::unsubscribe); + subscriptions.clear(); + if (row.getItem() != null) { + for (Map.Entry>> pseudoClassWithCondition : pseudoClasses.entrySet()) { + ObservableValue condition = pseudoClassWithCondition.getValue().call(row.getItem()); + subscriptions.add(BindingsHelper.includePseudoClassWhen( + row, + pseudoClassWithCondition.getKey(), + condition)); + } + } + }); return row; } diff --git a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java index 609a1aa4827..802eb62cdd5 100644 --- a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java +++ b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java @@ -18,7 +18,7 @@ import org.jabref.model.groups.RegexKeywordGroup; import org.jabref.model.groups.SearchGroup; import org.jabref.model.groups.TexGroup; -import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; public class GroupSerializer { @@ -70,9 +70,9 @@ private String serializeSearchGroup(SearchGroup group) { sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); sb.append(StringUtil.quote(group.getSearchExpression(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR)); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); - sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.SearchFlags.CASE_SENSITIVE))); + sb.append(StringUtil.booleanToBinaryString(false)); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); - sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.SearchFlags.REGULAR_EXPRESSION))); + sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchFlags.REGULAR_EXPRESSION))); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); appendGroupDetails(sb, group); diff --git a/src/main/java/org/jabref/logic/importer/util/GroupsParser.java b/src/main/java/org/jabref/logic/importer/util/GroupsParser.java index 642aadf1453..0e785d24f85 100644 --- a/src/main/java/org/jabref/logic/importer/util/GroupsParser.java +++ b/src/main/java/org/jabref/logic/importer/util/GroupsParser.java @@ -278,9 +278,7 @@ private static AbstractGroup searchGroupFromString(String s) { int context = Integer.parseInt(tok.nextToken()); String expression = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR); EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); - if (Integer.parseInt(tok.nextToken()) == 1) { - searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); - } + tok.nextToken(); // This used to be the flag for CASE_SENSITIVE search. Skip it for backwards-compatibility if (Integer.parseInt(tok.nextToken()) == 1) { searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); } diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java b/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java index a7457f327b1..e69de29bb2d 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java @@ -1,128 +0,0 @@ -package org.jabref.logic.pdf.search.indexing; - -import java.util.List; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.jabref.gui.util.BackgroundTask; -import org.jabref.gui.util.DefaultTaskExecutor; -import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.LinkedFile; - -/** - * Wrapper around {@link PdfIndexer} to execute all operations in the background. - */ -public class IndexingTaskManager extends BackgroundTask { - - private final Queue taskQueue = new ConcurrentLinkedQueue<>(); - private TaskExecutor taskExecutor; - private int numOfIndexedFiles = 0; - - private final Object lock = new Object(); - private boolean isRunning = false; - private boolean isBlockingNewTasks = false; - - public IndexingTaskManager(TaskExecutor taskExecutor) { - this.taskExecutor = taskExecutor; - showToUser(true); - willBeRecoveredAutomatically(true); - DefaultTaskExecutor.runInJavaFXThread(() -> { - this.updateProgress(1, 1); - this.titleProperty().set(Localization.lang("Indexing pdf files")); - }); - } - - @Override - protected Void call() throws Exception { - synchronized (lock) { - isRunning = true; - } - updateProgress(); - while (!taskQueue.isEmpty() && !isCanceled()) { - taskQueue.poll().run(); - numOfIndexedFiles++; - updateProgress(); - } - synchronized (lock) { - isRunning = false; - } - return null; - } - - private void updateProgress() { - DefaultTaskExecutor.runInJavaFXThread(() -> { - updateMessage(Localization.lang("%0 of %1 linked files added to the index", numOfIndexedFiles, numOfIndexedFiles + taskQueue.size())); - updateProgress(numOfIndexedFiles, numOfIndexedFiles + taskQueue.size()); - }); - } - - private void enqueueTask(Runnable indexingTask) { - if (!isBlockingNewTasks) { - taskQueue.add(indexingTask); - // What if already running? - synchronized (lock) { - if (!isRunning) { - isRunning = true; - this.executeWith(taskExecutor); - showToUser(false); - } - } - } - } - - public AutoCloseable blockNewTasks() { - synchronized (lock) { - isBlockingNewTasks = true; - } - return () -> { - synchronized (lock) { - isBlockingNewTasks = false; - } - }; - } - - public void createIndex(PdfIndexer indexer) { - enqueueTask(indexer::createIndex); - } - - 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)); - } - } - - public void addToIndex(PdfIndexer indexer, BibEntry entry, BibDatabaseContext databaseContext) { - enqueueTask(() -> addToIndex(indexer, entry, entry.getFiles(), databaseContext)); - } - - public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.addToIndex(entry, file, databaseContext)); - } - } - - public void removeFromIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.removeFromIndex(file.getLink())); - } - } - - public void removeFromIndex(PdfIndexer indexer, BibEntry entry) { - enqueueTask(() -> removeFromIndex(indexer, entry, entry.getFiles())); - } - - public void updateDatabaseName(String name) { - DefaultTaskExecutor.runInJavaFXThread(() -> this.titleProperty().set(Localization.lang("Indexing for %0", name))); - } -} diff --git a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java b/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java deleted file mode 100644 index 2fd7e54e5b0..00000000000 --- a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.jabref.logic.pdf.search.retrieval; - -import java.io.IOException; -import java.util.LinkedList; -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; -import org.jabref.model.strings.StringUtil; - -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexReader; -import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; -import org.apache.lucene.queryparser.classic.ParseException; -import org.apache.lucene.search.IndexSearcher; -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; - -import static org.jabref.model.pdf.search.SearchFieldConstants.PDF_FIELDS; - -public final class PdfSearcher { - - private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - - private final Directory indexDirectory; - - private PdfSearcher(Directory indexDirectory) { - this.indexDirectory = indexDirectory; - } - - public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOException { - return new PdfSearcher(new NIOFSDirectory(databaseContext.getFulltextIndexPath())); - } - - /** - * Search for results matching a query in the Lucene search index - * - * @param searchString a pattern to search for matching entries in the index, must not be null - * @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!"))) { - return new PdfSearchResults(); - } - if (maxHits <= 0) { - 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)) { - 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()); - 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..2cd968e89f9 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; @@ -32,7 +31,9 @@ public List getMatches() { return Collections.emptyList(); } - List matchEntries = database.getEntries().stream().filter(query::isMatch).collect(Collectors.toList()); + List matchEntries = List.of(); + // List matchEntries = database.getEntries().stream().filter(query::isMatch).collect(Collectors.toList()); + // TODO btut: is this for CLI? We need the databasecontext to access the index 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..6fe8b1a5dcf 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -1,197 +1,104 @@ package org.jabref.logic.search; -import java.util.Collections; +import java.util.Arrays; import java.util.EnumSet; -import java.util.List; +import java.util.HashMap; import java.util.Objects; -import java.util.Optional; -import java.util.regex.Pattern; +import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.jabref.logic.l10n.Localization; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.search.SearchMatcher; -import org.jabref.model.search.rules.ContainsBasedSearchRule; -import org.jabref.model.search.rules.GrammarBasedSearchRule; -import org.jabref.model.search.rules.SearchRule; + +import org.jabref.model.pdf.search.EnglishStemAnalyzer; +import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SentenceAnalyzer; -public class SearchQuery implements SearchMatcher { +import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.search.Query; - /** - * The mode of escaping special characters in regular expressions - */ - private enum EscapeMode { - /** - * using \Q and \E marks - */ - JAVA { - @Override - String format(String regex) { - return Pattern.quote(regex); - } - }, - /** - * escaping all javascript regex special characters separately - */ - JAVASCRIPT { - @Override - String format(String regex) { - return JAVASCRIPT_ESCAPED_CHARS_PATTERN.matcher(regex).replaceAll("\\\\$0"); - } - }; - - /** - * Regex pattern for escaping special characters in javascript regular expressions - */ - private static final Pattern JAVASCRIPT_ESCAPED_CHARS_PATTERN = Pattern.compile("[.*+?^${}()|\\[\\]\\\\/]"); - - /** - * Attempt to escape all regex special characters. - * - * @param regex a string containing a regex expression - * @return a regex with all special characters escaped - */ - abstract String format(String regex); - } +import static org.jabref.model.search.rules.SearchRules.SearchFlags.FILTERING_SEARCH; +import static org.jabref.model.search.rules.SearchRules.SearchFlags.KEEP_SEARCH_STRING; +import static org.jabref.model.search.rules.SearchRules.SearchFlags.SORT_BY_SCORE; + +public class SearchQuery { - private final String query; - private EnumSet searchFlags; - private final SearchRule rule; + protected final String query; + protected Query parsedQuery; + protected String parseError; + protected EnumSet searchFlags; public SearchQuery(String query, EnumSet searchFlags) { this.query = Objects.requireNonNull(query); this.searchFlags = searchFlags; - this.rule = SearchRules.getSearchRuleByQuery(query, searchFlags); - } - - @Override - public String toString() { - return String.format("\"%s\" (%s, %s)", getQuery(), getCaseSensitiveDescription(), getRegularExpressionDescription()); - } - - @Override - public boolean isMatch(BibEntry entry) { - return rule.applyRule(getQuery(), entry); - } - public boolean isValid() { - return rule.validateSearchStrings(getQuery()); - } + HashMap boosts = new HashMap<>(); + SearchFieldConstants.searchableBibFields.forEach(field -> boosts.put(field, Float.valueOf(4))); - public boolean isContainsBasedSearch() { - return rule instanceof ContainsBasedSearchRule; - } - - private String getCaseSensitiveDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return "case sensitive"; - } else { - return "case insensitive"; + if (searchFlags.contains(SearchRules.SearchFlags.FULLTEXT)) { + Arrays.stream(SearchFieldConstants.PDF_FIELDS).forEach(field -> boosts.put(field, Float.valueOf(1))); } - } + String[] fieldsToSearchArray = new String[boosts.size()]; + boosts.keySet().toArray(fieldsToSearchArray); - private String getRegularExpressionDescription() { if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - return "regular expression"; - } else { - return "plain text"; + if (query.length() > 0 && !(query.startsWith("/") && query.endsWith("/"))) { + query = "/" + query + "/"; + } } - } - - public String localize() { - return String.format("\"%s\" (%s, %s)", - getQuery(), - getLocalizedCaseSensitiveDescription(), - getLocalizedRegularExpressionDescription()); - } - private String getLocalizedCaseSensitiveDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return Localization.lang("case sensitive"); - } else { - return Localization.lang("case insensitive"); + MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fieldsToSearchArray, new EnglishStemAnalyzer(), boosts); + queryParser.setAllowLeadingWildcard(true); + if (!query.contains("\"") && !query.contains(":") && !query.contains("*") && !query.contains("~") & query.length() > 0) { + query = Arrays.stream(query.split(" ")).map(s -> "*" + s + "*").collect(Collectors.joining(" ")); } - } - - private String getLocalizedRegularExpressionDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - return Localization.lang("regular expression"); - } else { - return Localization.lang("plain text"); + try { + parsedQuery = queryParser.parse(query); + parseError = null; + } catch (ParseException e) { + parsedQuery = null; + parseError = e.getMessage(); } } - /** - * Tests if the query is an advanced search query described as described in the help - * - * @return true if the query is an advanced search query - */ - public boolean isGrammarBasedSearch() { - return rule instanceof GrammarBasedSearchRule; - } - - public String getQuery() { + @Override + public String toString() { return query; } - public EnumSet getSearchFlags() { - return searchFlags; - } - /** - * Returns a list of words this query searches for. The returned strings can be a regular expression. + * Equals, but only partially compares SearchFlags + * + * @return true if the search query is the same except for the filtering/sorting flags */ - public List getSearchWords() { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - return Collections.singletonList(getQuery()); - } else { - // Parses the search query for valid words and returns a list these words. - // For example, "The great Vikinger" will give ["The","great","Vikinger"] - return (new SentenceAnalyzer(getQuery())).getWords(); + @Override + public boolean equals(Object other) { + if (other instanceof SearchQuery searchQuery) { + if (!searchQuery.query.equals(this.query)) { + return false; + } + Set thisSearchRulesWithoutFilterAndSort = this.searchFlags.clone(); + Set otherSearchRulesWithoutFilterAndSort = searchQuery.searchFlags.clone(); + Set filterAndSortFlags = EnumSet.of(SORT_BY_SCORE, FILTERING_SEARCH, KEEP_SEARCH_STRING); + thisSearchRulesWithoutFilterAndSort.removeAll(filterAndSortFlags); + otherSearchRulesWithoutFilterAndSort.removeAll(filterAndSortFlags); + return thisSearchRulesWithoutFilterAndSort.equals(otherSearchRulesWithoutFilterAndSort); } + return false; } - // Returns a regular expression pattern in the form (w1)|(w2)| ... wi are escaped if no regular expression search is enabled - public Optional getPatternForWords() { - return joinWordsToPattern(EscapeMode.JAVA); + @Override + public int hashCode() { + return this.query.hashCode(); } - // Returns a regular expression pattern in the form (w1)|(w2)| ... wi are escaped for javascript if no regular expression search is enabled - public Optional getJavaScriptPatternForWords() { - return joinWordsToPattern(EscapeMode.JAVASCRIPT); + public boolean isValid() { + return parseError == null; } - /** - * Returns a regular expression pattern in the form (w1)|(w2)| ... wi are escaped if no regular expression search is enabled - * - * @param escapeMode the mode of escaping special characters in wi - */ - private Optional joinWordsToPattern(EscapeMode escapeMode) { - List words = getSearchWords(); - - if ((words == null) || words.isEmpty() || words.get(0).isEmpty()) { - return Optional.empty(); - } - - // compile the words to a regular expression in the form (w1)|(w2)|(w3) - Stream joiner = words.stream(); - if (!searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - // Reformat string when we are looking for a literal match - joiner = joiner.map(escapeMode::format); - } - String searchPattern = joiner.collect(Collectors.joining(")|(", "(", ")")); - - if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return Optional.of(Pattern.compile(searchPattern)); - } else { - return Optional.of(Pattern.compile(searchPattern, Pattern.CASE_INSENSITIVE)); - } + public Query getQuery() { + return parsedQuery; } - public SearchRule getRule() { - return rule; + public EnumSet getSearchFlags() { + return searchFlags; } } diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java b/src/main/java/org/jabref/logic/search/indexing/DocumentReader.java similarity index 99% rename from src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java rename to src/main/java/org/jabref/logic/search/indexing/DocumentReader.java index e2e1c0f5b7e..030b3fc43c9 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java +++ b/src/main/java/org/jabref/logic/search/indexing/DocumentReader.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.search.indexing; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/org/jabref/logic/search/indexing/IndexingTaskManager.java b/src/main/java/org/jabref/logic/search/indexing/IndexingTaskManager.java new file mode 100644 index 00000000000..5fd8a41075f --- /dev/null +++ b/src/main/java/org/jabref/logic/search/indexing/IndexingTaskManager.java @@ -0,0 +1,143 @@ +package org.jabref.logic.search.indexing; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; + +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.DefaultTaskExecutor; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; + +/** + * Wrapper around {@link LuceneIndexer} to execute all operations in the background. + */ +public class IndexingTaskManager extends BackgroundTask { + + private final ConcurrentLinkedDeque taskQueue = new ConcurrentLinkedDeque<>(); + private final TaskExecutor taskExecutor; + private int numOfIndexedFiles = 0; + + private final Object lock = new Object(); + private boolean isRunning = false; + private boolean isBlockingNewTasks = false; + + public IndexingTaskManager(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + showToUser(true); + willBeRecoveredAutomatically(true); + DefaultTaskExecutor.runInJavaFXThread(() -> { + this.updateProgress(1, 1); + this.titleProperty().set(Localization.lang("Indexing pdf files")); + }); + } + + @Override + protected Void call() throws Exception { + synchronized (lock) { + isRunning = true; + } + updateProgress(); + while (!taskQueue.isEmpty() && !isCanceled()) { + taskQueue.pollFirst().run(); + numOfIndexedFiles++; + updateProgress(); + } + synchronized (lock) { + isRunning = false; + } + return null; + } + + private void updateProgress() { + DefaultTaskExecutor.runInJavaFXThread(() -> { + updateMessage(Localization.lang("%0 of %1 entries added to the index", numOfIndexedFiles, numOfIndexedFiles + taskQueue.size())); + updateProgress(numOfIndexedFiles, numOfIndexedFiles + taskQueue.size()); + }); + } + + private void enqueueTask(Runnable indexingTask, boolean skipToFront) { + if (!isBlockingNewTasks) { + if (skipToFront) { + taskQueue.addFirst(indexingTask); + } else { + taskQueue.addLast(indexingTask); + } + // What if already running? + synchronized (lock) { + if (!isRunning) { + isRunning = true; + this.executeWith(taskExecutor); + showToUser(false); + } + } + } + } + + public AutoCloseable blockNewTasks() { + synchronized (lock) { + isBlockingNewTasks = true; + } + return () -> { + synchronized (lock) { + isBlockingNewTasks = false; + } + }; + } + + public void createIndex(LuceneIndexer indexer) { + enqueueTask(indexer::createIndex, true); + } + + public void manageFulltextIndexAccordingToPrefs(LuceneIndexer indexer) { + indexer.getFilePreferences().fulltextIndexLinkedFilesProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + for (BibEntry bibEntry : indexer.getDatabaseContext().getEntries()) { + enqueueTask(() -> indexer.updateLinkedFilesInIndex(bibEntry, List.of()), false); + } + } else { + enqueueTask(indexer::deleteLinkedFilesIndex, true); + } + }); + } + + public void updateIndex(LuceneIndexer indexer) { + Set pathsToRemove = indexer.getListOfFilePaths(); + Set hashesOfEntriesToRemove = indexer.getListOfHashes(); + indexer.getDatabaseContext().getEntries().forEach(BibEntry::updateAndGetIndexHash); + for (BibEntry entry : indexer.getDatabaseContext().getEntries()) { + enqueueTask(() -> indexer.addBibFieldsToIndex(entry), true); + enqueueTask(() -> indexer.addLinkedFilesToIndex(entry), false); + hashesOfEntriesToRemove.removeIf(hash -> Integer.valueOf(entry.getLastIndexHash()).equals(hash)); + for (LinkedFile file : entry.getFiles()) { + pathsToRemove.remove(file.getLink()); + } + } + for (String pathToRemove : pathsToRemove) { + enqueueTask(() -> indexer.removeFromIndex(pathToRemove), true); + } + for (int hashToRemove : hashesOfEntriesToRemove) { + enqueueTask(() -> indexer.removeFromIndex(hashToRemove), true); + } + } + + public void addToIndex(LuceneIndexer indexer, BibEntry entry) { + enqueueTask(() -> indexer.addBibFieldsToIndex(entry), true); + enqueueTask(() -> indexer.addLinkedFilesToIndex(entry), false); + } + + public void removeFromIndex(LuceneIndexer indexer, BibEntry entry) { + enqueueTask(() -> indexer.removeFromIndex(entry), false); + } + + public void updateIndex(LuceneIndexer indexer, BibEntry entry, List removedFiles) { + enqueueTask(() -> indexer.updateBibFieldsInIndex(entry), true); + enqueueTask(() -> indexer.updateLinkedFilesInIndex(entry, removedFiles), false); + } + + public void updateDatabaseName(String name) { + DefaultTaskExecutor.runInJavaFXThread(() -> this.titleProperty().set(Localization.lang("Indexing for %0", name))); + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java b/src/main/java/org/jabref/logic/search/indexing/LuceneIndexer.java similarity index 52% rename from src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java rename to src/main/java/org/jabref/logic/search/indexing/LuceneIndexer.java index 4fea4b638a5..83d174c46a1 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java +++ b/src/main/java/org/jabref/logic/search/indexing/LuceneIndexer.java @@ -1,31 +1,43 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.search.indexing; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.jabref.gui.LibraryTab; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.Keyword; +import org.jabref.model.entry.KeywordList; import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; import org.jabref.model.pdf.search.EnglishStemAnalyzer; import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; import org.apache.lucene.document.Document; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexNotFoundException; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.ScoreDoc; @@ -39,22 +51,27 @@ /** * Indexes the text of PDF files and adds it into the lucene search index. */ -public class PdfIndexer { +public class LuceneIndexer { private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); private final Directory directoryToIndex; - private BibDatabaseContext databaseContext; + private final BibDatabaseContext databaseContext; - private final FilePreferences filePreferences; + private final PreferencesService preferences; - public PdfIndexer(Directory indexDirectory, FilePreferences filePreferences) { - this.directoryToIndex = indexDirectory; - this.filePreferences = filePreferences; + public LuceneIndexer(BibDatabaseContext databaseContext, PreferencesService preferences) throws IOException { + this.databaseContext = databaseContext; + this.directoryToIndex = new NIOFSDirectory(databaseContext.getFulltextIndexPath()); + this.preferences = preferences; } - public static PdfIndexer of(BibDatabaseContext databaseContext, FilePreferences filePreferences) throws IOException { - return new PdfIndexer(new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); + public static LuceneIndexer of(BibDatabaseContext databaseContext, PreferencesService preferences) throws IOException { + return new LuceneIndexer(databaseContext, preferences); + } + + public BibDatabaseContext getDatabaseContext() { + return databaseContext; } /** @@ -70,46 +87,31 @@ public void createIndex() { } } - public void addToIndex(BibDatabaseContext databaseContext) { - for (BibEntry entry : databaseContext.getEntries()) { - addToIndex(entry, databaseContext); - } - } - /** * Adds all the 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); - } - - /** - * 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) { - for (LinkedFile linkedFile : linkedFiles) { - addToIndex(entry, linkedFile, databaseContext); + public void addLinkedFilesToIndex(BibEntry entry) { + for (LinkedFile file : entry.getFiles()) { + writeFileToIndex(entry, file); } } /** - * Adds a pdf file linked to one entry in the database to an existing (or new) Lucene search index + * Removes an entry identified by its hash from the index * - * @param entry a bibtex entry - * @param linkedFile the link to the pdf files + * @param hash the hash to be removed */ - public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext databaseContext) { - if (databaseContext != null) { - this.databaseContext = databaseContext; - } - if (!entry.getFiles().isEmpty()) { - writeToIndex(entry, linkedFile); + public void removeFromIndex(int hash) { + try (IndexWriter indexWriter = new IndexWriter( + directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { + indexWriter.deleteDocuments(new Term(SearchFieldConstants.BIB_ENTRY_ID_HASH, String.valueOf(hash))); + indexWriter.commit(); + } catch (IOException e) { + LOGGER.warn("Could not initialize the IndexWriter!", e); } } @@ -130,22 +132,13 @@ public void removeFromIndex(String linkedFilePath) { } } - /** - * Removes all files linked to a bib-entry from the index - * - * @param entry the entry documents are linked to - */ - public void removeFromIndex(BibEntry entry) { - removeFromIndex(entry, 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) { - for (LinkedFile linkedFile : linkedFiles) { + public void removeFromIndex(BibEntry entry) { + for (LinkedFile linkedFile : entry.getFiles()) { removeFromIndex(linkedFile.getLink()); } } @@ -163,15 +156,34 @@ public void flushIndex() { } } - /** - * Writes all files linked to an entry to the index if the files are not yet in the index or the files on the fs are - * newer than the one in the index. - * - * @param entry the entry associated with the file - */ - private void writeToIndex(BibEntry entry) { - for (LinkedFile linkedFile : entry.getFiles()) { - writeToIndex(entry, linkedFile); + public void addBibFieldsToIndex(BibEntry bibEntry) { + try { + try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { + Document document = new Document(); + document.add(new StringField(SearchFieldConstants.BIB_ENTRY_ID_HASH, String.valueOf(bibEntry.getLastIndexHash()), org.apache.lucene.document.Field.Store.YES)); + for (Map.Entry field : bibEntry.getFieldMap().entrySet()) { + SearchFieldConstants.searchableBibFields.add(field.getKey().getName()); + if (field.getKey() == StandardField.KEYWORDS) { + KeywordList keywords = KeywordList.parse(field.getValue(), preferences.getBibEntryPreferences().getKeywordSeparator()); + for (Keyword keyword : keywords) { + document.add(new StringField(field.getKey().getName(), keyword.toString(), org.apache.lucene.document.Field.Store.YES)); + } + } else if (field.getKey() == StandardField.GROUPS) { + List groups = Arrays.stream(field.getValue().split(preferences.getBibEntryPreferences().getKeywordSeparator().toString())).map(String::trim).toList(); + for (String group : groups) { + document.add(new StringField(field.getKey().getName(), group, org.apache.lucene.document.Field.Store.YES)); + } + } else { + document.add(new TextField(field.getKey().getName(), field.getValue(), org.apache.lucene.document.Field.Store.YES)); + } + } + indexWriter.addDocument(document); + indexWriter.commit(); + } + } catch (IOException e) { + LOGGER.warn("Could not add an entry to the index!", e); } } @@ -182,11 +194,14 @@ private void writeToIndex(BibEntry entry) { * @param entry the entry associated with the file * @param linkedFile the file to write to the index */ - private void writeToIndex(BibEntry entry, LinkedFile linkedFile) { + private void writeFileToIndex(BibEntry entry, LinkedFile linkedFile) { + if (!preferences.getFilePreferences().shouldFulltextIndexLinkedFiles()) { + return; + } if (linkedFile.isOnlineLink() || !StandardFileType.PDF.getName().equals(linkedFile.getFileType())) { return; } - Optional resolvedPath = linkedFile.findIn(databaseContext, filePreferences); + Optional resolvedPath = linkedFile.findIn(databaseContext, preferences.getFilePreferences()); if (resolvedPath.isEmpty()) { LOGGER.debug("Could not find {}", linkedFile.getLink()); return; @@ -212,7 +227,7 @@ private void writeToIndex(BibEntry entry, LinkedFile linkedFile) { // if there is no index yet, don't need to check anything! } // If no document was found, add the new one - Optional> pages = new DocumentReader(entry, filePreferences).readLinkedPdf(this.databaseContext, linkedFile); + Optional> pages = new DocumentReader(entry, preferences.getFilePreferences()).readLinkedPdf(this.databaseContext, linkedFile); if (pages.isPresent()) { try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, new IndexWriterConfig( @@ -226,24 +241,83 @@ private void writeToIndex(BibEntry entry, LinkedFile linkedFile) { } } + public void updateBibFieldsInIndex(BibEntry entry) { + int oldHash = entry.getLastIndexHash(); + int newHash = entry.updateAndGetIndexHash(); + if (oldHash != newHash) { + addBibFieldsToIndex(entry); + removeFromIndex(oldHash); + } + } + + public void updateLinkedFilesInIndex(BibEntry entry, List removedFiles) { + for (LinkedFile removedFile : removedFiles) { + removeFromIndex(removedFile.getLink()); + } + for (LinkedFile linkedFile : entry.getFiles()) { + writeFileToIndex(entry, linkedFile); + } + } + /** - * Lists the paths of all the files that are stored in the index + * Lists all values of a given field stored in the index * - * @return all file paths + * @param field the field to get the values for + * @return all values for this field */ - public Set getListOfFilePaths() { - Set paths = new HashSet<>(); + private Set getListOfField(String field) { + Set values = new HashSet<>(); try (IndexReader reader = DirectoryReader.open(directoryToIndex)) { IndexSearcher searcher = new IndexSearcher(reader); MatchAllDocsQuery query = new MatchAllDocsQuery(); TopDocs allDocs = searcher.search(query, Integer.MAX_VALUE); for (ScoreDoc scoreDoc : allDocs.scoreDocs) { Document doc = reader.document(scoreDoc.doc); - paths.add(doc.getField(SearchFieldConstants.PATH).stringValue()); + if (doc.getField(field) != null) { + values.add(doc.getField(field).stringValue()); + } } } catch (IOException e) { - return paths; + return values; + } + return values; + } + + /** + * Lists the paths of all the files that are stored in the index + * + * @return all file paths + */ + public Set getListOfFilePaths() { + return getListOfField(SearchFieldConstants.PATH); + } + + /** + * Lists the hashes of all the entries that are stored in the index + * + * @return all entry hashes + */ + public Set getListOfHashes() { + return getListOfField(SearchFieldConstants.BIB_ENTRY_ID_HASH).stream().map(Integer::valueOf).collect(Collectors.toSet()); + } + + public void deleteLinkedFilesIndex() { + try (IndexWriter indexWriter = new IndexWriter( + directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { + QueryParser queryParser = new QueryParser(SearchFieldConstants.PATH, new EnglishStemAnalyzer()); + queryParser.setAllowLeadingWildcard(true); + indexWriter.deleteDocuments(queryParser.parse("*")); + indexWriter.commit(); + } catch (IOException e) { + LOGGER.warn("Could not initialize the IndexWriter", e); + } catch (ParseException e) { + LOGGER.error("Could not parse", e); } - return paths; + } + + public FilePreferences getFilePreferences() { + return preferences.getFilePreferences(); } } diff --git a/src/main/java/org/jabref/logic/search/retrieval/LuceneSearcher.java b/src/main/java/org/jabref/logic/search/retrieval/LuceneSearcher.java new file mode 100644 index 00000000000..a224b5524f5 --- /dev/null +++ b/src/main/java/org/jabref/logic/search/retrieval/LuceneSearcher.java @@ -0,0 +1,67 @@ +package org.jabref.logic.search.retrieval; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Objects; + +import org.jabref.logic.search.SearchQuery; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.pdf.search.LuceneSearchResults; +import org.jabref.model.pdf.search.SearchResult; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.IndexSearcher; +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; + +public final class LuceneSearcher { + private static final Logger LOGGER = LoggerFactory.getLogger(LuceneSearcher.class); + + private final BibDatabaseContext databaseContext; + private final Directory indexDirectory; + + private LuceneSearcher(BibDatabaseContext databaseContext, Directory indexDirectory) { + this.databaseContext = databaseContext; + this.indexDirectory = indexDirectory; + } + + public static LuceneSearcher of(BibDatabaseContext databaseContext) throws IOException { + return new LuceneSearcher(databaseContext, new NIOFSDirectory(databaseContext.getFulltextIndexPath())); + } + + /** + * Search for results matching a query in the Lucene search index + * + * @param query query to search for + * @return a result map of all entries that have matches in any fields + */ + public HashMap search(SearchQuery query) { + Objects.requireNonNull(query); + + HashMap results = new HashMap<>(); + try (IndexReader reader = DirectoryReader.open(indexDirectory)) { + IndexSearcher searcher = new IndexSearcher(reader); + TopDocs docs = searcher.search(query.getQuery(), Integer.MAX_VALUE); + for (ScoreDoc scoreDoc : docs.scoreDocs) { + SearchResult searchResult = new SearchResult(searcher, query.getQuery(), scoreDoc); + for (BibEntry match : searchResult.getMatchingEntries(databaseContext)) { + if (searchResult.getLuceneScore() > 0) { + if (!results.containsKey(match)) { + results.put(match, new LuceneSearchResults()); + } + results.get(match).addResult(searchResult); + } + } + } + } catch (IOException e) { + LOGGER.warn("Could not open Index at: '{}'!\n{}", indexDirectory, e.getMessage()); + } + return results; + } +} diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 022e64319ba..c2d846d2789 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -96,6 +96,7 @@ public class BibEntry implements Cloneable { public static final EntryType DEFAULT_TYPE = StandardEntryType.Misc; private static final Logger LOGGER = LoggerFactory.getLogger(BibEntry.class); private final SharedBibEntryData sharedBibEntryData; + private int lastIndexHash; /** * Map to store the words in every field @@ -921,6 +922,15 @@ public int hashCode() { return Objects.hash(commentsBeforeEntry, type.getValue(), fields); } + public int getLastIndexHash() { + return lastIndexHash; + } + + public int updateAndGetIndexHash() { + lastIndexHash = hashCode(); + return lastIndexHash; + } + public void registerListener(Object object) { this.eventBus.register(object); } diff --git a/src/main/java/org/jabref/model/groups/SearchGroup.java b/src/main/java/org/jabref/model/groups/SearchGroup.java index c1df365bef2..f0fa5bdb102 100644 --- a/src/main/java/org/jabref/model/groups/SearchGroup.java +++ b/src/main/java/org/jabref/model/groups/SearchGroup.java @@ -1,10 +1,16 @@ package org.jabref.model.groups; +import java.io.IOException; +import java.nio.file.Path; import java.util.EnumSet; import java.util.Objects; +import java.util.Set; +import org.jabref.logic.search.retrieval.LuceneSearcher; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.search.GroupSearchQuery; +import org.jabref.model.search.rules.SearchRules; import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.slf4j.Logger; @@ -17,6 +23,7 @@ public class SearchGroup extends AbstractGroup { private static final Logger LOGGER = LoggerFactory.getLogger(SearchGroup.class); + private Set matches = Set.of(); private final GroupSearchQuery query; public SearchGroup(String name, GroupHierarchyType context, String searchExpression, EnumSet searchFlags) { @@ -45,10 +52,10 @@ public boolean equals(Object o) { @Override public boolean contains(BibEntry entry) { - return query.isMatch(entry); + return matches.contains(entry); } - public EnumSet getSearchFlags() { + public EnumSet getSearchFlags() { return query.getSearchFlags(); } @@ -79,4 +86,12 @@ public boolean isDynamic() { public int hashCode() { return Objects.hash(getName(), getHierarchicalContext(), getSearchExpression(), getSearchFlags()); } + + public void updateMatches(BibDatabaseContext context) { + try { + this.matches = LuceneSearcher.of(context).search(query).keySet(); + } catch (IOException e) { + LOGGER.warn("Could not open Index for: '{}'!\n{}", context.getDatabasePath().orElse(Path.of("unsaved")).toAbsolutePath(), e.getMessage()); + } + } } diff --git a/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java b/src/main/java/org/jabref/model/pdf/search/LuceneSearchResults.java similarity index 67% rename from src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java rename to src/main/java/org/jabref/model/pdf/search/LuceneSearchResults.java index db38bfe9548..36738440228 100644 --- a/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java +++ b/src/main/java/org/jabref/model/pdf/search/LuceneSearchResults.java @@ -2,20 +2,14 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; -public final class PdfSearchResults { +public final class LuceneSearchResults { - private final List searchResults; - - public PdfSearchResults(List search) { - this.searchResults = Collections.unmodifiableList(search); - } - - public PdfSearchResults() { - this.searchResults = Collections.emptyList(); - } + private final List searchResults = new LinkedList<>(); public List getSortedByScore() { List sortedList = new ArrayList<>(searchResults); @@ -44,4 +38,16 @@ public HashMap> getSearchResultsByPath() { public int numSearchResults() { return this.searchResults.size(); } + + public void addResult(SearchResult result) { + this.searchResults.add(result); + } + + public float getSearchScore() { + return this.searchResults.stream().map(SearchResult::getLuceneScore).max(Comparator.comparing(Float::floatValue)).orElse(Float.valueOf(0)); + } + + public boolean hasFulltextResults() { + return this.searchResults.stream().map(SearchResult::hasFulltextResults).anyMatch(Boolean::valueOf); + } } diff --git a/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java index b4fa07c5d34..9b1f738204d 100644 --- a/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java +++ b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java @@ -1,14 +1,21 @@ package org.jabref.model.pdf.search; +import java.util.HashSet; +import java.util.Set; + public class SearchFieldConstants { - public static final String PATH = "path"; + public static final String FILE_FIELDS_PREFIX = "f_"; + public static final String BIB_ENTRY_ID_HASH = "id_hash"; + + public static final String PATH = FILE_FIELDS_PREFIX + "path"; public static final String CONTENT = "content"; - public static final String PAGE_NUMBER = "pageNumber"; + public static final String PAGE_NUMBER = FILE_FIELDS_PREFIX + "pageNumber"; public static final String ANNOTATIONS = "annotations"; - public static final String MODIFIED = "modified"; + public static final String MODIFIED = FILE_FIELDS_PREFIX + "modified"; - public static final String[] PDF_FIELDS = new String[]{PATH, CONTENT, PAGE_NUMBER, MODIFIED, ANNOTATIONS}; + public static final String[] PDF_FIELDS = new String[]{PATH, CONTENT, ANNOTATIONS}; + public static Set searchableBibFields = new HashSet<>(); public static final String VERSION = "95"; } diff --git a/src/main/java/org/jabref/model/pdf/search/SearchResult.java b/src/main/java/org/jabref/model/pdf/search/SearchResult.java index 7a79191e94c..b2e4a29ec18 100644 --- a/src/main/java/org/jabref/model/pdf/search/SearchResult.java +++ b/src/main/java/org/jabref/model/pdf/search/SearchResult.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.stream.Collectors; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.index.IndexableField; @@ -19,6 +21,7 @@ import org.apache.lucene.search.highlight.TextFragment; import static org.jabref.model.pdf.search.SearchFieldConstants.ANNOTATIONS; +import static org.jabref.model.pdf.search.SearchFieldConstants.BIB_ENTRY_ID_HASH; import static org.jabref.model.pdf.search.SearchFieldConstants.CONTENT; import static org.jabref.model.pdf.search.SearchFieldConstants.MODIFIED; import static org.jabref.model.pdf.search.SearchFieldConstants.PAGE_NUMBER; @@ -30,35 +33,59 @@ public final class SearchResult { private final int pageNumber; private final long modified; + private final int hash; + private final boolean hasFulltextResults; private final float luceneScore; - private List contentResultStringsHtml; - private List annotationsResultStringsHtml; + private List contentResultStringsHtml = List.of(); + private List annotationsResultStringsHtml = List.of(); public SearchResult(IndexSearcher searcher, Query query, ScoreDoc scoreDoc) throws IOException { - this.path = getFieldContents(searcher, scoreDoc, PATH); - this.pageNumber = Integer.parseInt(getFieldContents(searcher, scoreDoc, PAGE_NUMBER)); - this.modified = Long.parseLong(getFieldContents(searcher, scoreDoc, MODIFIED)); this.luceneScore = scoreDoc.score; - - String content = getFieldContents(searcher, scoreDoc, CONTENT); - String annotations = getFieldContents(searcher, scoreDoc, ANNOTATIONS); - - Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("", ""), new QueryScorer(query)); - - try (TokenStream contentStream = new EnglishStemAnalyzer().tokenStream(CONTENT, content)) { - TextFragment[] frags = highlighter.getBestTextFragments(contentStream, content, true, 10); - this.contentResultStringsHtml = Arrays.stream(frags).map(TextFragment::toString).collect(Collectors.toList()); - } catch (InvalidTokenOffsetsException e) { - this.contentResultStringsHtml = List.of(); + this.path = getFieldContents(searcher, scoreDoc, PATH); + if (this.path.length() > 0) { + // pdf result + this.pageNumber = Integer.parseInt(getFieldContents(searcher, scoreDoc, PAGE_NUMBER)); + this.modified = Long.parseLong(getFieldContents(searcher, scoreDoc, MODIFIED)); + this.hash = 0; + + String content = getFieldContents(searcher, scoreDoc, CONTENT); + String annotations = getFieldContents(searcher, scoreDoc, ANNOTATIONS); + this.hasFulltextResults = !(content.isEmpty() && annotations.isEmpty()); + + Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("", ""), new QueryScorer(query)); + + try (TokenStream contentStream = new EnglishStemAnalyzer().tokenStream(CONTENT, content)) { + TextFragment[] frags = highlighter.getBestTextFragments(contentStream, content, true, 10); + this.contentResultStringsHtml = Arrays.stream(frags).map(TextFragment::toString).collect(Collectors.toList()); + } catch (InvalidTokenOffsetsException e) { + this.contentResultStringsHtml = List.of(); + } + + try (TokenStream annotationStream = new EnglishStemAnalyzer().tokenStream(ANNOTATIONS, annotations)) { + TextFragment[] frags = highlighter.getBestTextFragments(annotationStream, annotations, true, 10); + this.annotationsResultStringsHtml = Arrays.stream(frags).map(TextFragment::toString).collect(Collectors.toList()); + } catch (InvalidTokenOffsetsException e) { + this.annotationsResultStringsHtml = List.of(); + } + } else { + // Found somewhere in the bib entry + this.hash = Integer.parseInt(getFieldContents(searcher, scoreDoc, BIB_ENTRY_ID_HASH)); + this.pageNumber = -1; + this.modified = -1; + this.hasFulltextResults = false; } + } - try (TokenStream annotationStream = new EnglishStemAnalyzer().tokenStream(ANNOTATIONS, annotations)) { - TextFragment[] frags = highlighter.getBestTextFragments(annotationStream, annotations, true, 10); - this.annotationsResultStringsHtml = Arrays.stream(frags).map(TextFragment::toString).collect(Collectors.toList()); - } catch (InvalidTokenOffsetsException e) { - this.annotationsResultStringsHtml = List.of(); + public List getMatchingEntries(BibDatabaseContext databaseContext) { + if (this.path.length() > 0) { + return getEntriesWithFile(path, databaseContext); } + return databaseContext.getEntries().stream().filter(bibEntry -> bibEntry.getLastIndexHash() == this.hash).collect(Collectors.toList()); + } + + private List getEntriesWithFile(String path, BibDatabaseContext databaseContext) { + return databaseContext.getEntries().stream().filter(entry -> entry.getFiles().stream().map(LinkedFile::getLink).anyMatch(link -> link.equals(path))).collect(Collectors.toList()); } private String getFieldContents(IndexSearcher searcher, ScoreDoc scoreDoc, String field) throws IOException { @@ -69,8 +96,11 @@ private String getFieldContents(IndexSearcher searcher, ScoreDoc scoreDoc, Strin return indexableField.stringValue(); } - public boolean isResultFor(BibEntry entry) { - return entry.getFiles().stream().anyMatch(linkedFile -> path.equals(linkedFile.getLink())); + public float getSearchScoreFor(BibEntry entry) { + if (this.path != null) { + return entry.getFiles().stream().anyMatch(linkedFile -> path.equals(linkedFile.getLink())) ? luceneScore : 0; + } + return entry.getLastIndexHash() == hash ? luceneScore : 0; } public String getPath() { @@ -96,4 +126,8 @@ public List getAnnotationsResultStringsHtml() { public int getPageNumber() { return pageNumber; } + + public boolean hasFulltextResults() { + return this.hasFulltextResults; + } } diff --git a/src/main/java/org/jabref/model/search/GroupSearchQuery.java b/src/main/java/org/jabref/model/search/GroupSearchQuery.java index 90056d9b0f7..cbc999204a0 100644 --- a/src/main/java/org/jabref/model/search/GroupSearchQuery.java +++ b/src/main/java/org/jabref/model/search/GroupSearchQuery.java @@ -1,65 +1,17 @@ package org.jabref.model.search; import java.util.EnumSet; -import java.util.Objects; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.search.rules.SearchRule; -import org.jabref.model.search.rules.SearchRules; +import org.jabref.logic.search.SearchQuery; import org.jabref.model.search.rules.SearchRules.SearchFlags; -public class GroupSearchQuery implements SearchMatcher { - - private final String query; - private final EnumSet searchFlags; - private final SearchRule rule; +public class GroupSearchQuery extends SearchQuery { public GroupSearchQuery(String query, EnumSet searchFlags) { - this.query = Objects.requireNonNull(query); - this.searchFlags = searchFlags; - this.rule = Objects.requireNonNull(getSearchRule()); - } - - @Override - public String toString() { - return String.format("\"%s\" (%s, %s)", query, getCaseSensitiveDescription(), - getRegularExpressionDescription()); - } - - @Override - public boolean isMatch(BibEntry entry) { - return this.getRule().applyRule(query, entry); - } - - private SearchRule getSearchRule() { - return SearchRules.getSearchRuleByQuery(query, searchFlags); - } - - private String getCaseSensitiveDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - return "case sensitive"; - } else { - return "case insensitive"; - } - } - - private String getRegularExpressionDescription() { - if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { - return "regular expression"; - } else { - return "plain text"; - } - } - - public SearchRule getRule() { - return rule; + super(query, searchFlags); } public String getSearchExpression() { return query; } - - public EnumSet getSearchFlags() { - return searchFlags; - } } diff --git a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java deleted file mode 100644 index 618d1771b95..00000000000 --- a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; - -import org.jabref.architecture.AllowedToUseLogic; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.model.strings.StringUtil; - -/** - * Search rule for a search based on String.contains() - */ -@AllowedToUseLogic("Because access to the lucene index is needed") -public class ContainsBasedSearchRule extends FullTextSearchRule { - - public ContainsBasedSearchRule(EnumSet searchFlags) { - super(searchFlags); - } - - @Override - public boolean validateSearchStrings(String query) { - return true; - } - - @Override - public boolean applyRule(String query, BibEntry bibEntry) { - String searchString = query; - if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - searchString = searchString.toLowerCase(Locale.ROOT); - } - - List unmatchedWords = new SentenceAnalyzer(searchString).getWords(); - - for (Field fieldKey : bibEntry.getFields()) { - String formattedFieldContent = StringUtil.stripAccents(bibEntry.getFieldLatexFree(fieldKey).get()); - if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - formattedFieldContent = formattedFieldContent.toLowerCase(Locale.ROOT); - } - - Iterator unmatchedWordsIterator = unmatchedWords.iterator(); - while (unmatchedWordsIterator.hasNext()) { - String word = StringUtil.stripAccents(unmatchedWordsIterator.next()); - if (formattedFieldContent.contains(word)) { - unmatchedWordsIterator.remove(); - } - } - - if (unmatchedWords.isEmpty()) { - return true; - } - } - - return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. - } -} diff --git a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java deleted file mode 100644 index 27cfbf65b8b..00000000000 --- a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.jabref.model.search.rules; - -import java.io.IOException; -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.model.entry.BibEntry; -import org.jabref.model.pdf.search.PdfSearchResults; -import org.jabref.model.pdf.search.SearchResult; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * All classes providing full text search results inherit from this class. - *

- * Some kind of caching of the full text search results is implemented. - */ -@AllowedToUseLogic("Because access to the lucene index is needed") -public abstract class FullTextSearchRule implements SearchRule { - - private static final Logger LOGGER = LoggerFactory.getLogger(FullTextSearchRule.class); - - protected final EnumSet searchFlags; - - protected String lastQuery; - protected List lastSearchResults; - - private final BibDatabaseContext databaseContext; - - public FullTextSearchRule(EnumSet searchFlags) { - this.searchFlags = searchFlags; - this.lastQuery = ""; - lastSearchResults = Collections.emptyList(); - - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); - } - - public EnumSet getSearchFlags() { - return searchFlags; - } - - @Override - public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { - return new PdfSearchResults(); - } - - if (!query.equals(this.lastQuery)) { - this.lastQuery = query; - lastSearchResults = Collections.emptyList(); - try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); - PdfSearchResults results = searcher.search(query, 5); - lastSearchResults = results.getSortedByScore(); - } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); - } - } - - return new PdfSearchResults(lastSearchResults.stream() - .filter(searchResult -> searchResult.isResultFor(bibEntry)) - .collect(Collectors.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 deleted file mode 100644 index fd281a11a0f..00000000000 --- a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java +++ /dev/null @@ -1,283 +0,0 @@ -package org.jabref.model.search.rules; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -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; -import org.jabref.search.SearchBaseVisitor; -import org.jabref.search.SearchLexer; -import org.jabref.search.SearchParser; - -import org.antlr.v4.runtime.ANTLRInputStream; -import org.antlr.v4.runtime.BailErrorStrategy; -import org.antlr.v4.runtime.BaseErrorListener; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.Recognizer; -import org.antlr.v4.runtime.misc.ParseCancellationException; -import org.antlr.v4.runtime.tree.ParseTree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The search query must be specified in an expression that is acceptable by the Search.g4 grammar. - *

- * 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 { - - private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); - - private final EnumSet searchFlags; - - private ParseTree tree; - 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(); - - @Override - public void syntaxError(Recognizer recognizer, Object offendingSymbol, - int line, int charPositionInLine, String msg, RecognitionException e) - throws ParseCancellationException { - throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); - } - } - - public GrammarBasedSearchRule(EnumSet searchFlags) throws RecognitionException { - this.searchFlags = searchFlags; - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); - } - - public static boolean isValid(EnumSet searchFlags, String query) { - return new GrammarBasedSearchRule(searchFlags).validateSearchStrings(query); - } - - public ParseTree getTree() { - return this.tree; - } - - public String getQuery() { - return this.query; - } - - private void init(String query) throws ParseCancellationException { - if (Objects.equals(this.query, query)) { - return; - } - - SearchLexer lexer = new SearchLexer(new ANTLRInputStream(query)); - lexer.removeErrorListeners(); // no infos on file system - lexer.addErrorListener(ThrowingErrorListener.INSTANCE); - 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 - 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 - 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; - } - } - - @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 { - init(query); - return true; - } catch (ParseCancellationException e) { - LOGGER.debug("Search query invalid", e); - return false; - } - } - - public EnumSet getSearchFlags() { - return searchFlags; - } - - public enum ComparisonOperator { - EXACT, CONTAINS, DOES_NOT_CONTAIN; - - public static ComparisonOperator build(String value) { - if ("CONTAINS".equalsIgnoreCase(value) || "=".equals(value)) { - return CONTAINS; - } else if ("MATCHES".equalsIgnoreCase(value) || "==".equals(value)) { - return EXACT; - } else { - return DOES_NOT_CONTAIN; - } - } - } - - public static class Comparator { - - private final ComparisonOperator operator; - private final Pattern fieldPattern; - private final Pattern valuePattern; - - public Comparator(String field, String value, ComparisonOperator operator, EnumSet searchFlags) { - this.operator = operator; - - int option = searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE; - this.fieldPattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? StringUtil.stripAccents(field) : "\\Q" + StringUtil.stripAccents(field) + "\\E", option); - this.valuePattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? StringUtil.stripAccents(value) : "\\Q" + StringUtil.stripAccents(value) + "\\E", option); - } - - public boolean compare(BibEntry entry) { - // special case for searching for entrytype=phdthesis - if (fieldPattern.matcher(InternalField.TYPE_HEADER.getName()).matches()) { - return matchFieldValue(entry.getType().getName()); - } - - // special case for searching a single keyword - if (fieldPattern.matcher("anykeyword").matches()) { - return entry.getKeywords(',').stream().map(Keyword::toString).anyMatch(this::matchFieldValue); - } - - // specification of fieldsKeys to search is done in the search expression itself - Set fieldsKeys = entry.getFields(); - - // special case for searching allfields=cat and title=dog - if (!fieldPattern.matcher("anyfield").matches()) { - // Filter out the requested fields - fieldsKeys = fieldsKeys.stream().filter(matchFieldKey()).collect(Collectors.toSet()); - } - - for (Field field : fieldsKeys) { - Optional fieldValue = entry.getFieldLatexFree(field); - if (fieldValue.isPresent()) { - if (matchFieldValue(StringUtil.stripAccents(fieldValue.get()))) { - return true; - } - } - } - - // special case of asdf!=whatever and entry does not contain asdf - return fieldsKeys.isEmpty() && (operator == ComparisonOperator.DOES_NOT_CONTAIN); - } - - private Predicate matchFieldKey() { - return field -> fieldPattern.matcher(field.getName()).matches(); - } - - public boolean matchFieldValue(String content) { - Matcher matcher = valuePattern.matcher(content); - if (operator == ComparisonOperator.CONTAINS) { - return matcher.find(); - } else if (operator == ComparisonOperator.EXACT) { - return matcher.matches(); - } else if (operator == ComparisonOperator.DOES_NOT_CONTAIN) { - return !matcher.find(); - } else { - throw new IllegalStateException("MUST NOT HAPPEN"); - } - } - } - - /** - * Search results in boolean. It may be later on converted to an int. - */ - static class BibtexSearchVisitor extends SearchBaseVisitor { - - private final EnumSet searchFlags; - - private final BibEntry entry; - - public BibtexSearchVisitor(EnumSet searchFlags, BibEntry bibEntry) { - this.searchFlags = searchFlags; - this.entry = bibEntry; - } - - public boolean comparison(String field, ComparisonOperator operator, String value) { - return new Comparator(field, value, operator, searchFlags).compare(entry); - } - - @Override - public Boolean visitStart(SearchParser.StartContext ctx) { - return visit(ctx.expression()); - } - - @Override - public Boolean visitComparison(SearchParser.ComparisonContext context) { - // remove possible enclosing " symbols - String right = context.right.getText(); - if (right.startsWith("\"") && right.endsWith("\"")) { - right = right.substring(1, right.length() - 1); - } - - Optional fieldDescriptor = Optional.ofNullable(context.left); - if (fieldDescriptor.isPresent()) { - return comparison(fieldDescriptor.get().getText(), ComparisonOperator.build(context.operator.getText()), right); - } else { - return SearchRules.getSearchRule(searchFlags).applyRule(right, entry); - } - } - - @Override - public Boolean visitUnaryExpression(SearchParser.UnaryExpressionContext ctx) { - return !visit(ctx.expression()); // negate - } - - @Override - public Boolean visitParenExpression(SearchParser.ParenExpressionContext ctx) { - return visit(ctx.expression()); // ignore parenthesis - } - - @Override - public Boolean visitBinaryExpression(SearchParser.BinaryExpressionContext ctx) { - if ("AND".equalsIgnoreCase(ctx.operator.getText())) { - return visit(ctx.left) && visit(ctx.right); // and - } else { - return visit(ctx.left) || visit(ctx.right); // or - } - } - } -} diff --git a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java deleted file mode 100644 index 3454c0f68dc..00000000000 --- a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.EnumSet; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import org.jabref.architecture.AllowedToUseLogic; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.model.strings.StringUtil; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Search rule for regex-based search. - */ -@AllowedToUseLogic("Because access to the lucene index is needed") -public class RegexBasedSearchRule extends FullTextSearchRule { - - private static final Logger LOGGER = LoggerFactory.getLogger(RegexBasedSearchRule.class); - - public RegexBasedSearchRule(EnumSet searchFlags) { - super(searchFlags); - } - - @Override - public boolean validateSearchStrings(String query) { - try { - Pattern.compile(query, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); - } catch (PatternSyntaxException ex) { - return false; - } - return true; - } - - @Override - public boolean applyRule(String query, BibEntry bibEntry) { - Pattern pattern; - try { - pattern = Pattern.compile(StringUtil.stripAccents(query), searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); - } catch (PatternSyntaxException ex) { - LOGGER.debug("Could not compile regex {}", query, ex); - return false; - } - - for (Field field : bibEntry.getFields()) { - Optional fieldOptional = bibEntry.getField(field); - if (fieldOptional.isPresent()) { - String fieldContentNoBrackets = StringUtil.stripAccents(bibEntry.getFieldLatexFree(field).get()); - Matcher m = pattern.matcher(fieldContentNoBrackets); - if (m.find()) { - return true; - } - } - } - return getFulltextResults(query, bibEntry).numSearchResults() > 0; - } -} diff --git a/src/main/java/org/jabref/model/search/rules/SearchRule.java b/src/main/java/org/jabref/model/search/rules/SearchRule.java deleted file mode 100644 index 1be2b05b342..00000000000 --- a/src/main/java/org/jabref/model/search/rules/SearchRule.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.jabref.model.search.rules; - -import org.jabref.model.entry.BibEntry; -import org.jabref.model.pdf.search.PdfSearchResults; - -public interface SearchRule { - - boolean applyRule(String query, BibEntry bibEntry); - - PdfSearchResults getFulltextResults(String query, BibEntry bibEntry); - - boolean validateSearchStrings(String query); -} diff --git a/src/main/java/org/jabref/model/search/rules/SearchRules.java b/src/main/java/org/jabref/model/search/rules/SearchRules.java index ba28e66b133..708ca1df316 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -1,49 +1,11 @@ package org.jabref.model.search.rules; -import java.util.EnumSet; -import java.util.regex.Pattern; - /** * This is a factory to instantiate the matching SearchRule implementation matching a given query */ public class SearchRules { - private static final Pattern SIMPLE_EXPRESSION = Pattern.compile("[^\\p{Punct}]*"); - - private SearchRules() { - } - - /** - * Returns the appropriate search rule that fits best to the given parameter. - */ - public static SearchRule getSearchRuleByQuery(String query, EnumSet searchFlags) { - if (isSimpleQuery(query)) { - return new ContainsBasedSearchRule(searchFlags); - } - - // this searches specified fields if specified, - // and all fields otherwise - SearchRule searchExpression = new GrammarBasedSearchRule(searchFlags); - if (searchExpression.validateSearchStrings(query)) { - return searchExpression; - } else { - return getSearchRule(searchFlags); - } - } - - private static boolean isSimpleQuery(String query) { - return SIMPLE_EXPRESSION.matcher(query).matches(); - } - - static SearchRule getSearchRule(EnumSet searchFlags) { - if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) { - return new RegexBasedSearchRule(searchFlags); - } else { - return new ContainsBasedSearchRule(searchFlags); - } - } - public enum SearchFlags { - CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT, KEEP_SEARCH_STRING + REGULAR_EXPRESSION, FULLTEXT, KEEP_SEARCH_STRING, FILTERING_SEARCH, SORT_BY_SCORE; } } diff --git a/src/main/java/org/jabref/model/search/rules/SentenceAnalyzer.java b/src/main/java/org/jabref/model/search/rules/SentenceAnalyzer.java deleted file mode 100644 index 89987d9d5ac..00000000000 --- a/src/main/java/org/jabref/model/search/rules/SentenceAnalyzer.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.ArrayList; -import java.util.List; - -public class SentenceAnalyzer { - - public static final char ESCAPE_CHAR = '\\'; - public static final char QUOTE_CHAR = '"'; - - private final String query; - - public SentenceAnalyzer(String query) { - this.query = query; - } - - public List getWords() { - List result = new ArrayList<>(); - - StringBuilder stringBuilder = new StringBuilder(); - boolean escaped = false; - boolean quoted = false; - for (char c : query.toCharArray()) { - // Check if we are entering an escape sequence: - if (!escaped && c == ESCAPE_CHAR) { - escaped = true; - } else { - // See if we have reached the end of a word: - if (!escaped && !quoted && Character.isWhitespace(c)) { - if (stringBuilder.length() > 0) { - result.add(stringBuilder.toString()); - stringBuilder = new StringBuilder(); - } - } else if (c == QUOTE_CHAR) { - // Whether it is a start or end quote, store the current - // word if any: - if (stringBuilder.length() > 0) { - result.add(stringBuilder.toString()); - stringBuilder = new StringBuilder(); - } - quoted = !quoted; - } else { - // All other possibilities exhausted, we add the char to - // the current word: - stringBuilder.append(c); - } - escaped = false; - } - } - // Finished with the loop. If we have a current word, add it: - if (stringBuilder.length() > 0) { - result.add(stringBuilder.toString()); - } - - return result; - } -} diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 64eb1845322..e4699254599 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -54,7 +54,6 @@ import org.jabref.gui.maintable.NameDisplayPreferences.DisplayStyle; import org.jabref.gui.mergeentries.DiffMode; import org.jabref.gui.push.PushToApplications; -import org.jabref.gui.search.SearchDisplayMode; import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.specialfields.SpecialFieldsPreferences; import org.jabref.gui.theme.Theme; @@ -113,7 +112,7 @@ import org.jabref.model.groups.GroupHierarchyType; import org.jabref.model.metadata.SaveOrder; import org.jabref.model.metadata.SelfContainedSaveOrder; -import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; import com.github.javakeyring.Keyring; @@ -262,14 +261,14 @@ public class JabRefPreferences implements PreferencesService { public static final String FILE_BROWSER_COMMAND = "fileBrowserCommand"; public static final String MAIN_FILE_DIRECTORY = "fileDirectory"; - public static final String SEARCH_DISPLAY_MODE = "searchDisplayMode"; - public static final String SEARCH_CASE_SENSITIVE = "caseSensitiveSearch"; public static final String SEARCH_REG_EXP = "regExpSearch"; public static final String SEARCH_FULLTEXT = "fulltextSearch"; public static final String SEARCH_KEEP_SEARCH_STRING = "keepSearchString"; public static final String SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP = "keepOnTop"; public static final String SEARCH_WINDOW_HEIGHT = "searchWindowHeight"; public static final String SEARCH_WINDOW_WIDTH = "searchWindowWidth"; + public static final String SEARCH_FILTERING_MODE = "filteringSearch"; + public static final String SEARCH_SORT_BY_SCORE = "sortByScore"; public static final String IMPORTERS_ENABLED = "importersEnabled"; public static final String GENERATE_KEY_ON_IMPORT = "generateKeyOnImport"; @@ -406,7 +405,9 @@ public class JabRefPreferences implements PreferencesService { private static final String PROTECTED_TERMS_DISABLED_INTERNAL = "protectedTermsDisabledInternal"; // GroupViewMode - private static final String GROUP_INTERSECT_UNION_VIEW_MODE = "groupIntersectUnionViewModes"; + private static final String GROUP_VIEW_INTERSECTION = "groupIntersection"; + private static final String GROUP_VIEW_FILTER = "groupFilter"; + private static final String GROUP_VIEW_INVERT = "groupInvert"; private static final String DEFAULT_HIERARCHICAL_CONTEXT = "defaultHierarchicalContext"; // Dialog states @@ -499,17 +500,17 @@ private JabRefPreferences() { // Since some of the preference settings themselves use localized strings, we cannot set the language after // the initialization of the preferences in main // Otherwise that language framework will be instantiated and more importantly, statically initialized preferences - // like the SearchDisplayMode will never be translated. + // will never be translated. Localization.setLanguage(getLanguage()); - defaults.put(SEARCH_DISPLAY_MODE, SearchDisplayMode.FILTER.toString()); - defaults.put(SEARCH_CASE_SENSITIVE, Boolean.FALSE); defaults.put(SEARCH_REG_EXP, Boolean.FALSE); defaults.put(SEARCH_FULLTEXT, Boolean.FALSE); defaults.put(SEARCH_KEEP_SEARCH_STRING, Boolean.FALSE); defaults.put(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP, Boolean.TRUE); defaults.put(SEARCH_WINDOW_HEIGHT, 176.0); defaults.put(SEARCH_WINDOW_WIDTH, 600.0); + defaults.put(SEARCH_SORT_BY_SCORE, Boolean.TRUE); + defaults.put(SEARCH_FILTERING_MODE, Boolean.FALSE); defaults.put(IMPORTERS_ENABLED, Boolean.TRUE); defaults.put(GENERATE_KEY_ON_IMPORT, Boolean.TRUE); @@ -642,7 +643,9 @@ private JabRefPreferences() { defaults.put(AUTOCOMPLETER_COMPLETE_FIELDS, "author;editor;title;journal;publisher;keywords;crossref;related;entryset"); defaults.put(AUTO_ASSIGN_GROUP, Boolean.TRUE); defaults.put(DISPLAY_GROUP_COUNT, Boolean.TRUE); - defaults.put(GROUP_INTERSECT_UNION_VIEW_MODE, GroupViewMode.INTERSECTION.name()); + defaults.put(GROUP_VIEW_INTERSECTION, Boolean.TRUE); + defaults.put(GROUP_VIEW_FILTER, Boolean.TRUE); + defaults.put(GROUP_VIEW_INVERT, Boolean.FALSE); defaults.put(DEFAULT_HIERARCHICAL_CONTEXT, GroupHierarchyType.INDEPENDENT.name()); defaults.put(KEYWORD_SEPARATOR, ", "); defaults.put(DEFAULT_ENCODING, StandardCharsets.UTF_8.name()); @@ -1404,13 +1407,22 @@ public GroupsPreferences getGroupsPreferences() { } groupsPreferences = new GroupsPreferences( - GroupViewMode.valueOf(get(GROUP_INTERSECT_UNION_VIEW_MODE)), + getBoolean(GROUP_VIEW_INTERSECTION), + getBoolean(GROUP_VIEW_FILTER), + getBoolean(GROUP_VIEW_INVERT), getBoolean(AUTO_ASSIGN_GROUP), getBoolean(DISPLAY_GROUP_COUNT), GroupHierarchyType.valueOf(get(DEFAULT_HIERARCHICAL_CONTEXT)) ); - EasyBind.listen(groupsPreferences.groupViewModeProperty(), (obs, oldValue, newValue) -> put(GROUP_INTERSECT_UNION_VIEW_MODE, newValue.name())); + groupsPreferences.groupViewModeProperty().addListener(new SetChangeListener() { + @Override + public void onChanged(Change change) { + putBoolean(GROUP_VIEW_INTERSECTION, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INTERSECTION)); + putBoolean(GROUP_VIEW_FILTER, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.FILTER)); + putBoolean(GROUP_VIEW_INVERT, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INVERT)); + } + }); EasyBind.listen(groupsPreferences.autoAssignGroupProperty(), (obs, oldValue, newValue) -> putBoolean(AUTO_ASSIGN_GROUP, newValue)); EasyBind.listen(groupsPreferences.displayGroupCountProperty(), (obs, oldValue, newValue) -> putBoolean(DISPLAY_GROUP_COUNT, newValue)); EasyBind.listen(groupsPreferences.defaultHierarchicalContextProperty(), (obs, oldValue, newValue) -> put(DEFAULT_HIERARCHICAL_CONTEXT, newValue.name())); @@ -2623,30 +2635,22 @@ public SearchPreferences getSearchPreferences() { return searchPreferences; } - SearchDisplayMode searchDisplayMode; - try { - searchDisplayMode = SearchDisplayMode.valueOf(get(SEARCH_DISPLAY_MODE)); - } catch (IllegalArgumentException ex) { - // Should only occur when the searchmode is set directly via preferences.put and the enum was not used - searchDisplayMode = SearchDisplayMode.valueOf((String) defaults.get(SEARCH_DISPLAY_MODE)); - } - searchPreferences = new SearchPreferences( - searchDisplayMode, - getBoolean(SEARCH_CASE_SENSITIVE), getBoolean(SEARCH_REG_EXP), getBoolean(SEARCH_FULLTEXT), getBoolean(SEARCH_KEEP_SEARCH_STRING), + getBoolean(SEARCH_FILTERING_MODE), + getBoolean(SEARCH_SORT_BY_SCORE), getBoolean(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP), getDouble(SEARCH_WINDOW_HEIGHT), getDouble(SEARCH_WINDOW_WIDTH)); - EasyBind.listen(searchPreferences.searchDisplayModeProperty(), (obs, oldValue, newValue) -> put(SEARCH_DISPLAY_MODE, Objects.requireNonNull(searchPreferences.getSearchDisplayMode()).toString())); - searchPreferences.getObservableSearchFlags().addListener((SetChangeListener) c -> { - putBoolean(SEARCH_CASE_SENSITIVE, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.CASE_SENSITIVE)); - putBoolean(SEARCH_REG_EXP, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - putBoolean(SEARCH_FULLTEXT, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT)); - putBoolean(SEARCH_KEEP_SEARCH_STRING, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.KEEP_SEARCH_STRING)); + searchPreferences.getObservableSearchFlags().addListener((SetChangeListener) c -> { + putBoolean(SEARCH_REG_EXP, searchPreferences.getObservableSearchFlags().contains(SearchFlags.REGULAR_EXPRESSION)); + putBoolean(SEARCH_FULLTEXT, searchPreferences.getObservableSearchFlags().contains(SearchFlags.FULLTEXT)); + putBoolean(SEARCH_KEEP_SEARCH_STRING, searchPreferences.getObservableSearchFlags().contains(SearchFlags.KEEP_SEARCH_STRING)); + putBoolean(SEARCH_FILTERING_MODE, searchPreferences.getObservableSearchFlags().contains(SearchFlags.FILTERING_SEARCH)); + putBoolean(SEARCH_SORT_BY_SCORE, searchPreferences.getObservableSearchFlags().contains(SearchFlags.SORT_BY_SCORE)); }); EasyBind.listen(searchPreferences.keepWindowOnTopProperty(), (obs, oldValue, newValue) -> putBoolean(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP, searchPreferences.shouldKeepWindowOnTop())); diff --git a/src/main/java/org/jabref/preferences/SearchPreferences.java b/src/main/java/org/jabref/preferences/SearchPreferences.java index a4fdbf9cb53..dd4b23b7b97 100644 --- a/src/main/java/org/jabref/preferences/SearchPreferences.java +++ b/src/main/java/org/jabref/preferences/SearchPreferences.java @@ -4,83 +4,64 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; -import org.jabref.gui.search.SearchDisplayMode; -import org.jabref.model.search.rules.SearchRules.SearchFlags; +import org.jabref.model.search.rules.SearchRules; public class SearchPreferences { - private final ObjectProperty searchDisplayMode; - private final ObservableSet searchFlags; + private final ObservableSet searchFlags; private final BooleanProperty keepWindowOnTop; private final DoubleProperty searchWindowHeight = new SimpleDoubleProperty(); private final DoubleProperty searchWindowWidth = new SimpleDoubleProperty(); - public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext, boolean isKeepSearchString, boolean keepWindowOnTop, double searchWindowHeight, double searchWindowWidth) { - this.searchDisplayMode = new SimpleObjectProperty<>(searchDisplayMode); + public SearchPreferences(boolean isRegularExpression, boolean isFulltext, boolean isKeepSearchString, boolean isFilteringMode, boolean isSortByScore, boolean keepWindowOnTop, double searchWindowHeight, double searchWindowWidth) { this.keepWindowOnTop = new SimpleBooleanProperty(keepWindowOnTop); - searchFlags = FXCollections.observableSet(EnumSet.noneOf(SearchFlags.class)); - if (isCaseSensitive) { - searchFlags.add(SearchFlags.CASE_SENSITIVE); - } + searchFlags = FXCollections.observableSet(EnumSet.noneOf(SearchRules.SearchFlags.class)); if (isRegularExpression) { - searchFlags.add(SearchFlags.REGULAR_EXPRESSION); + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); } if (isFulltext) { - searchFlags.add(SearchFlags.FULLTEXT); + searchFlags.add(SearchRules.SearchFlags.FULLTEXT); } if (isKeepSearchString) { - searchFlags.add(SearchFlags.KEEP_SEARCH_STRING); + searchFlags.add(SearchRules.SearchFlags.KEEP_SEARCH_STRING); + } + if (isFilteringMode) { + searchFlags.add(SearchRules.SearchFlags.FILTERING_SEARCH); + } + if (isSortByScore) { + searchFlags.add(SearchRules.SearchFlags.SORT_BY_SCORE); } this.setSearchWindowHeight(searchWindowHeight); this.setSearchWindowWidth(searchWindowWidth); } - public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags, boolean keepWindowOnTop) { - this.searchDisplayMode = new SimpleObjectProperty<>(searchDisplayMode); + public SearchPreferences(EnumSet searchFlags, boolean keepWindowOnTop) { this.keepWindowOnTop = new SimpleBooleanProperty(keepWindowOnTop); this.searchFlags = FXCollections.observableSet(searchFlags); } - public EnumSet getSearchFlags() { + public EnumSet getSearchFlags() { // copy of returns an exception when the EnumSet is empty if (searchFlags.isEmpty()) { - return EnumSet.noneOf(SearchFlags.class); + return EnumSet.noneOf(SearchRules.SearchFlags.class); } return EnumSet.copyOf(searchFlags); } - protected ObservableSet getObservableSearchFlags() { + public ObservableSet getObservableSearchFlags() { return searchFlags; } - public SearchDisplayMode getSearchDisplayMode() { - return searchDisplayMode.get(); - } - - public ObjectProperty searchDisplayModeProperty() { - return searchDisplayMode; - } - - public void setSearchDisplayMode(SearchDisplayMode searchDisplayMode) { - this.searchDisplayMode.set(searchDisplayMode); - } - - public boolean isCaseSensitive() { - return searchFlags.contains(SearchFlags.CASE_SENSITIVE); - } - - public void setSearchFlag(SearchFlags flag, boolean value) { + public void setSearchFlag(SearchRules.SearchFlags flag, boolean value) { if (searchFlags.contains(flag) && !value) { searchFlags.remove(flag); } else if (!searchFlags.contains(flag) && value) { @@ -89,15 +70,23 @@ public void setSearchFlag(SearchFlags flag, boolean value) { } public boolean isRegularExpression() { - return searchFlags.contains(SearchFlags.REGULAR_EXPRESSION); + return searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION); } public boolean isFulltext() { - return searchFlags.contains(SearchFlags.FULLTEXT); + return searchFlags.contains(SearchRules.SearchFlags.FULLTEXT); } public boolean shouldKeepSearchString() { - return searchFlags.contains(SearchFlags.KEEP_SEARCH_STRING); + return searchFlags.contains(SearchRules.SearchFlags.KEEP_SEARCH_STRING); + } + + public boolean isFilteringMode() { + return searchFlags.contains(SearchRules.SearchFlags.FILTERING_SEARCH); + } + + public boolean isSortByScore() { + return searchFlags.contains(SearchRules.SearchFlags.SORT_BY_SCORE); } public boolean shouldKeepWindowOnTop() { diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index d0533a0aa11..5a4ddd091ff 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -380,6 +380,9 @@ Format\:\ Tab\:field;field;...\ (e.g.\ General\:url;pdf;note...)=Format\: Tab\:f Format\ of\ author\ and\ editor\ names=Format of author and editor names Format\ string=Format string +Score=Score +Filter\ by\ groups=Filter by groups +Invert\ groups=Invert groups Formatter\ name=Formatter name @@ -387,6 +390,10 @@ found\ in\ AUX\ file=found in AUX file Fulltext\ for=Fulltext for +Filter\ search\ results=Filter search results + +Always\ sort\ by\ score=Always sort by score + Further\ information\ about\ Mr.\ DLib\ for\ JabRef\ users.=Further information about Mr. DLib for JabRef users. General=General @@ -455,7 +462,7 @@ I\ Agree=I Agree Indexing\ pdf\ files=Indexing pdf files Indexing\ for\ %0=Indexing for %0 -%0\ of\ %1\ linked\ files\ added\ to\ the\ index=%0 of %1 linked files added to the index +%0\ of\ %1\ entries\ added\ to\ the\ index=%0 of %1 entries added to the index Invalid\ citation\ key=Invalid citation key @@ -794,6 +801,7 @@ Unable\ to\ save\ library=Unable to save library Always\ reformat\ library\ on\ save\ and\ export=Always reformat library on save and export Character\ encoding\ '%0'\ is\ not\ supported.=Character encoding '%0' is not supported. +Search\ score=Search score Search=Search Searching...=Searching... Finished\ Searching=Finished Searching @@ -2110,6 +2118,8 @@ Hierarchical\ keyword\ delimiter=Hierarchical keyword delimiter Escape\ ampersands=Escape ampersands Escape\ dollar\ sign=Escape dollar sign +Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\:Smith\ AND\ title\:electrical=Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor:Smith AND title:electrical + Copied\ '%0'\ to\ clipboard.=Copied '%0' to clipboard. This\ operation\ requires\ an\ open\ library.=This operation requires an open library. diff --git a/src/test/java/org/jabref/cli/ArgumentProcessorTest.java b/src/test/java/org/jabref/cli/ArgumentProcessorTest.java index 55c9520b30a..5112830301d 100644 --- a/src/test/java/org/jabref/cli/ArgumentProcessorTest.java +++ b/src/test/java/org/jabref/cli/ArgumentProcessorTest.java @@ -49,7 +49,7 @@ void setup() { when(preferencesService.getImporterPreferences()).thenReturn(importerPreferences); when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); when(preferencesService.getSearchPreferences()).thenReturn( - new SearchPreferences(null, EnumSet.noneOf(SearchRules.SearchFlags.class), false) + new SearchPreferences(EnumSet.noneOf(SearchRules.SearchFlags.class), false) ); } diff --git a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java index 1d255cf7536..7e5a80fe675 100644 --- a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java @@ -11,8 +11,8 @@ 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.preferences.OwnerPreferences; +import org.jabref.logic.search.indexing.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; @@ -89,10 +89,9 @@ void setUp() { dialogService, stateManager, themeManager, - indexingTaskManager, taskExecutor, - journalAbbreviationRepository - ); + journalAbbreviationRepository, + indexingTaskManager); } @Test diff --git a/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java index f05b398d285..6592e469e47 100644 --- a/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java +++ b/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java @@ -45,7 +45,9 @@ void setUp() { taskExecutor = new CurrentThreadTaskExecutor(); preferencesService = mock(PreferencesService.class); when(preferencesService.getGroupsPreferences()).thenReturn(new GroupsPreferences( - GroupViewMode.UNION, + false, + true, + false, true, true, GroupHierarchyType.INDEPENDENT diff --git a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java index bdfe4b85bee..7af55d2a22b 100644 --- a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java +++ b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java @@ -1,5 +1,6 @@ package org.jabref.gui.groups; +import java.util.EnumSet; import java.util.Optional; import org.jabref.gui.DialogService; @@ -46,9 +47,9 @@ void setUp() { dialogService = mock(DialogService.class, Answers.RETURNS_DEEP_STUBS); when(preferencesService.getGroupsPreferences()).thenReturn(new GroupsPreferences( - GroupViewMode.UNION, - true, - true, + EnumSet.of(GroupViewMode.FILTER), + true, + true, GroupHierarchyType.INDEPENDENT)); groupTree = new GroupTreeViewModel(stateManager, mock(DialogService.class), preferencesService, taskExecutor, new CustomLocalDragboard()); } @@ -61,7 +62,7 @@ void rootGroupIsAllEntriesByDefault() { @Test void rootGroupIsSelectedByDefault() { - assertEquals(groupTree.rootGroupProperty().get().getGroupNode(), stateManager.getSelectedGroup(databaseContext).get(0)); + assertEquals(groupTree.rootGroupProperty().get().getGroupNode(), stateManager.getSelectedGroups(databaseContext).get(0)); } @Test diff --git a/src/test/java/org/jabref/gui/maintable/MainTableDataModelTest.java b/src/test/java/org/jabref/gui/maintable/MainTableDataModelTest.java index 3e9ddc469cb..d29ba9614ce 100644 --- a/src/test/java/org/jabref/gui/maintable/MainTableDataModelTest.java +++ b/src/test/java/org/jabref/gui/maintable/MainTableDataModelTest.java @@ -12,6 +12,7 @@ import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; +import org.jabref.gui.StateManager; import org.jabref.logic.bibtex.comparator.EntryComparator; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -33,7 +34,7 @@ void additionToObservableMapTriggersUpdate() { NameDisplayPreferences nameDisplayPreferences = new NameDisplayPreferences(NameDisplayPreferences.DisplayStyle.AS_IS, NameDisplayPreferences.AbbreviationStyle.FULL); SimpleObjectProperty fieldValueFormatter = new SimpleObjectProperty<>(new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); ObservableList entriesViewModel = EasyBind.mapBacked(allEntries, entry -> - new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter)); + new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter, new StateManager())); FilteredList entriesFiltered = new FilteredList<>(entriesViewModel); IntegerProperty resultSize = new SimpleIntegerProperty(); resultSize.bind(Bindings.size(entriesFiltered)); diff --git a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java b/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java deleted file mode 100644 index bc3406ec43c..00000000000 --- a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.jabref.gui.search; - -import java.util.EnumSet; -import java.util.List; - -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; -import javafx.stage.Stage; - -import org.jabref.gui.search.rules.describer.ContainsAndRegexBasedSearchRuleDescriber; -import org.jabref.gui.util.TooltipTextUtil; -import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.testutils.category.GUITest; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; - -@GUITest -@ExtendWith(ApplicationExtension.class) -class ContainsAndRegexBasedSearchRuleDescriberTest { - - @Start - void onStart(Stage stage) { - // Needed to init JavaFX thread - stage.show(); - } - - @Test - void testSimpleTerm() { - String query = "test"; - List expectedTexts = List.of( - TooltipTextUtil.createText("This search contains entries in which any field contains the term "), - TooltipTextUtil.createText("test", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testNoAst() { - String query = "a b"; - List expectedTexts = List.of( - TooltipTextUtil.createText("This search contains entries in which any field contains the term "), - TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" and "), - TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testNoAstRegex() { - String query = "a b"; - List expectedTexts = List.of( - TooltipTextUtil.createText("This search contains entries in which any field contains the regular expression "), - TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" and "), - TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testNoAstRegexCaseSensitive() { - String query = "a b"; - List expectedTexts = List.of( - TooltipTextUtil.createText("This search contains entries in which any field contains the regular expression "), - TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" and "), - TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testNoAstCaseSensitive() { - String query = "a b"; - List expectedTexts = List.of( - TooltipTextUtil.createText("This search contains entries in which any field contains the term "), - TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" and "), - TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE), query).getDescription(); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } -} diff --git a/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java b/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java deleted file mode 100644 index ce2c4cc3a49..00000000000 --- a/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.jabref.gui.search; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; - -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; -import javafx.stage.Stage; - -import org.jabref.gui.search.rules.describer.GrammarBasedSearchRuleDescriber; -import org.jabref.gui.util.TooltipTextUtil; -import org.jabref.model.search.rules.GrammarBasedSearchRule; -import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; -import org.jabref.testutils.category.GUITest; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -@GUITest -@ExtendWith(ApplicationExtension.class) -class GrammarBasedSearchRuleDescriberTest { - - @Start - void onStart(Stage stage) { - // Needed to init JavaFX thread - stage.show(); - } - - private TextFlow createDescription(String query, EnumSet searchFlags) { - GrammarBasedSearchRule grammarBasedSearchRule = new GrammarBasedSearchRule(searchFlags); - assertTrue(grammarBasedSearchRule.validateSearchStrings(query)); - GrammarBasedSearchRuleDescriber describer = new GrammarBasedSearchRuleDescriber(searchFlags, grammarBasedSearchRule.getTree()); - return describer.getDescription(); - } - - @Test - void testSimpleQueryCaseSensitiveRegex() { - String query = "a=b"; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), - TooltipTextUtil.createText("The search is case-sensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testSimpleQueryCaseSensitive() { - String query = "a=b"; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), - TooltipTextUtil.createText("The search is case-sensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testSimpleQuery() { - String query = "a=b"; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), - TooltipTextUtil.createText("The search is case-insensitive.")); - TextFlow description = createDescription(query, EnumSet.noneOf(SearchFlags.class)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testSimpleQueryRegex() { - String query = "a=b"; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), - TooltipTextUtil.createText("The search is case-insensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testComplexQueryCaseSensitiveRegex() { - String query = "not a=b and c=e or e=\"x\""; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), - TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), - TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case-sensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testComplexQueryRegex() { - String query = "not a=b and c=e or e=\"x\""; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), - TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), - TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case-insensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testComplexQueryCaseSensitive() { - String query = "not a=b and c=e or e=\"x\""; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case-sensitive.")); - TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } - - @Test - void testComplexQuery() { - String query = "not a=b and c=e or e=\"x\""; - List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), - TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case-insensitive.")); - TextFlow description = createDescription(query, EnumSet.noneOf(SearchFlags.class)); - - TextFlowEqualityHelper.assertEquals(expectedTexts, description); - } -} diff --git a/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java b/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java index da190c5de0a..17a79e94ce0 100644 --- a/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java +++ b/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java @@ -90,14 +90,14 @@ void serializeSingleRegexKeywordGroup() { @Test void serializeSingleSearchGroup() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); List serialization = groupSerializer.serializeTree(GroupTreeNode.fromGroup(group)); assertEquals(Collections.singletonList("0 SearchGroup:myExplicitGroup;0;author=harrer;1;1;1;;;;"), serialization); } @Test void serializeSingleSearchGroupWithRegex() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INCLUDING, "author=\"harrer\"", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INCLUDING, "author=\"harrer\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); List serialization = groupSerializer.serializeTree(GroupTreeNode.fromGroup(group)); assertEquals(Collections.singletonList("0 SearchGroup:myExplicitGroup;2;author=\"harrer\";1;0;1;;;;"), serialization); } diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java b/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java index 982504c9938..2eb2517ceb8 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.jabref.logic.search.indexing.DocumentReader; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java b/src/test/java/org/jabref/logic/pdf/search/indexing/LuceneIndexerTest.java similarity index 75% rename from src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java rename to src/test/java/org/jabref/logic/pdf/search/indexing/LuceneIndexerTest.java index 629bb293778..5cd9156e715 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/LuceneIndexerTest.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.Optional; +import org.jabref.logic.search.indexing.LuceneIndexer; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -12,6 +13,7 @@ import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; @@ -25,15 +27,18 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class PdfIndexerTest { +public class LuceneIndexerTest { - private PdfIndexer indexer; + private LuceneIndexer indexer; private BibDatabase database; private BibDatabaseContext context = mock(BibDatabaseContext.class); @BeforeEach public void setUp(@TempDir Path indexDir) throws IOException { FilePreferences filePreferences = mock(FilePreferences.class); + when(filePreferences.shouldFulltextIndexLinkedFiles()).thenReturn(true); + PreferencesService preferencesService = mock(PreferencesService.class); + when(preferencesService.getFilePreferences()).thenReturn(filePreferences); this.database = new BibDatabase(); this.context = mock(BibDatabaseContext.class); @@ -42,7 +47,7 @@ public void setUp(@TempDir Path indexDir) throws IOException { when(context.getFulltextIndexPath()).thenReturn(indexDir); when(context.getDatabase()).thenReturn(database); when(context.getEntries()).thenReturn(database.getEntries()); - this.indexer = PdfIndexer.of(context, filePreferences); + this.indexer = LuceneIndexer.of(context, preferencesService); } @Test @@ -54,11 +59,14 @@ public void exampleThesisIndex() throws IOException { // when indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, reader.numDocs()); + assertEquals(34, reader.numDocs()); } } @@ -71,11 +79,14 @@ public void dontIndexNonPdf() throws IOException { // when indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(0, reader.numDocs()); + assertEquals(1, reader.numDocs()); } } @@ -88,11 +99,14 @@ public void dontIndexOnlineLinks() throws IOException { // when indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(0, reader.numDocs()); + assertEquals(1, reader.numDocs()); } } @@ -106,11 +120,14 @@ public void exampleThesisIndexWithKey() throws IOException { // when indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, reader.numDocs()); + assertEquals(34, reader.numDocs()); } } @@ -124,11 +141,14 @@ public void metaDataIndex() throws IOException { // when indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(1, reader.numDocs()); + assertEquals(2, reader.numDocs()); } } @@ -141,10 +161,13 @@ public void testFlushIndex() throws IOException { database.insertEntry(entry); indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // index actually exists try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, reader.numDocs()); + assertEquals(34, reader.numDocs()); } // when @@ -164,11 +187,14 @@ public void exampleThesisIndexAppendMetaData() throws IOException { exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(exampleThesis); indexer.createIndex(); - indexer.addToIndex(context); + for (BibEntry bibEntry : context.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } // index with first entry try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, reader.numDocs()); + assertEquals(34, reader.numDocs()); } BibEntry metadata = new BibEntry(StandardEntryType.Article); @@ -176,11 +202,12 @@ public void exampleThesisIndexAppendMetaData() throws IOException { metadata.setFiles(Collections.singletonList(new LinkedFile("Metadata file", "metaData.pdf", StandardFileType.PDF.getName()))); // when - indexer.addToIndex(metadata, null); + indexer.addBibFieldsToIndex(metadata); + indexer.addLinkedFilesToIndex(metadata); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(34, reader.numDocs()); + assertEquals(36, reader.numDocs()); } } } diff --git a/src/test/java/org/jabref/logic/pdf/search/retrieval/LuceneSearcherTest.java b/src/test/java/org/jabref/logic/pdf/search/retrieval/LuceneSearcherTest.java new file mode 100644 index 00000000000..7743ecdfea8 --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/retrieval/LuceneSearcherTest.java @@ -0,0 +1,145 @@ +package org.jabref.logic.pdf.search.retrieval; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; + +import org.jabref.logic.search.SearchQuery; +import org.jabref.logic.search.indexing.LuceneIndexer; +import org.jabref.logic.search.retrieval.LuceneSearcher; +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.types.StandardEntryType; +import org.jabref.model.pdf.search.LuceneSearchResults; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LuceneSearcherTest { + + private LuceneSearcher searcher; + private PreferencesService preferencesService; + private BibDatabase bibDatabase; + private BibDatabaseContext bibDatabaseContext; + + @BeforeEach + public void setUp(@TempDir Path indexDir) throws IOException { + preferencesService = mock(PreferencesService.class); + when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); + + bibDatabase = new BibDatabase(); + bibDatabaseContext = mock(BibDatabaseContext.class); + when(bibDatabaseContext.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + when(bibDatabaseContext.getFulltextIndexPath()).thenReturn(indexDir); + when(bibDatabaseContext.getDatabase()).thenReturn(bibDatabase); + when(bibDatabaseContext.getEntries()).thenReturn(bibDatabase.getEntries()); + } + + private void initIndexer() throws IOException { + LuceneIndexer indexer = LuceneIndexer.of(bibDatabaseContext, preferencesService); + searcher = LuceneSearcher.of(bibDatabaseContext); + + for (BibEntry bibEntry : bibDatabaseContext.getEntries()) { + indexer.addBibFieldsToIndex(bibEntry); + indexer.addLinkedFilesToIndex(bibEntry); + } + } + + @Test + public void searchForTest() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(8, hits); + } + + @Test + public void searchForUniversity() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("University", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(1, hits); + } + + @Test + public void searchForStopWord() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("and", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(0, hits); + } + + @Test + public void searchForSecond() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("second", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(4, hits); + } + + @Test + public void searchForAnnotation() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("annotation", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(2, hits); + } + + @Test + public void searchForEmptyString() throws IOException { + insertPdfsForSearch(); + initIndexer(); + + HashMap searchResults = searcher.search(new SearchQuery("", EnumSet.noneOf(SearchRules.SearchFlags.class))); + int hits = searchResults.keySet().stream().mapToInt((key) -> searchResults.get(key).numSearchResults()).sum(); + assertEquals(0, hits); + } + + @Test + public void searchWithNullString() { + assertThrows(NullPointerException.class, () -> searcher.search(null)); + } + + private void insertPdfsForSearch() { + when(preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()).thenReturn(true); + + BibEntry examplePdf = new BibEntry(StandardEntryType.Article); + examplePdf.setFiles(Collections.singletonList(new LinkedFile("Example Entry", "example.pdf", StandardFileType.PDF.getName()))); + bibDatabase.insertEntry(examplePdf); + + BibEntry metaDataEntry = new BibEntry(StandardEntryType.Article); + metaDataEntry.setFiles(Collections.singletonList(new LinkedFile("Metadata Entry", "metaData.pdf", StandardFileType.PDF.getName()))); + metaDataEntry.setCitationKey("MetaData2017"); + bibDatabase.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"); + bibDatabase.insertEntry(exampleThesis); + } +} diff --git a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java b/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java deleted file mode 100644 index e005c271ff1..00000000000 --- a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.jabref.logic.pdf.search.retrieval; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; - -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; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.LinkedFile; -import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.model.pdf.search.PdfSearchResults; -import org.jabref.preferences.FilePreferences; - -import org.apache.lucene.queryparser.classic.ParseException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class PdfSearcherTest { - - private PdfSearcher search; - - @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()))); - 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"); - 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"); - database.insertEntry(exampleThesis); - - PdfIndexer indexer = PdfIndexer.of(context, filePreferences); - search = PdfSearcher.of(context); - - indexer.createIndex(); - indexer.addToIndex(context); - } - - @Test - public void searchForTest() throws IOException, ParseException { - PdfSearchResults result = search.search("test", 10); - assertEquals(8, result.numSearchResults()); - } - - @Test - public void searchForUniversity() throws IOException, ParseException { - PdfSearchResults result = search.search("University", 10); - assertEquals(1, result.numSearchResults()); - } - - @Test - public void searchForStopWord() throws IOException, ParseException { - PdfSearchResults result = search.search("and", 10); - assertEquals(0, result.numSearchResults()); - } - - @Test - public void searchForSecond() throws IOException, ParseException { - PdfSearchResults result = search.search("second", 10); - assertEquals(4, result.numSearchResults()); - } - - @Test - public void searchForAnnotation() throws IOException, ParseException { - PdfSearchResults result = search.search("annotation", 10); - assertEquals(2, result.numSearchResults()); - } - - @Test - public void searchForEmptyString() throws IOException { - PdfSearchResults result = search.search("", 10); - assertEquals(0, result.numSearchResults()); - } - - @Test - public void searchWithNullString() throws IOException { - assertThrows(NullPointerException.class, () -> search.search(null, 10)); - } - - @Test - public void searchForZeroResults() throws IOException { - assertThrows(IllegalArgumentException.class, () -> search.search("test", 0)); - } -} diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java index f276f9ddfaf..2b86997d101 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -17,7 +17,7 @@ public class DatabaseSearcherTest { - public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); private BibDatabase database; @@ -28,7 +28,7 @@ public void setUp() { @Test public void testNoMatchesFromEmptyDatabase() { - List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -41,7 +41,7 @@ public void testNoMatchesFromEmptyDatabaseWithInvalidSearchExpression() { @Test public void testGetDatabaseFromMatchesDatabaseWithEmptyEntries() { database.insertEntry(new BibEntry()); - List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -50,7 +50,7 @@ public void testNoMatchesFromDatabaseWithArticleTypeEntry() { BibEntry entry = new BibEntry(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "harrer"); database.insertEntry(entry); - List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -59,13 +59,13 @@ public void testCorrectMatchFromDatabaseWithArticleTypeEntry() { BibEntry entry = new BibEntry(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "harrer"); database.insertEntry(entry); - List matches = new DatabaseSearcher(new SearchQuery("harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("harrer", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.singletonList(entry), matches); } @Test public void testNoMatchesFromEmptyDatabaseWithInvalidQuery() { - SearchQuery query = new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchQuery query = new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); @@ -78,7 +78,7 @@ public void testCorrectMatchFromDatabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); List matches = new DatabaseSearcher(query, database).getMatches(); assertEquals(Collections.singletonList(entry), matches); @@ -93,7 +93,7 @@ public void testNoMatchesFromDatabaseWithTwoEntries() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.singletonList(entry), databaseSearcher.getMatches()); @@ -105,7 +105,7 @@ public void testNoMatchesFromDabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchQuery query = new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); @@ -116,7 +116,7 @@ public void testNoMatchFromDatabaseWithEmptyEntry() { BibEntry entry = new BibEntry(); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(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/LuceneTest.java b/src/test/java/org/jabref/logic/search/LuceneTest.java new file mode 100644 index 00000000000..1acfa23a1c4 --- /dev/null +++ b/src/test/java/org/jabref/logic/search/LuceneTest.java @@ -0,0 +1,53 @@ +package org.jabref.logic.search; + +import org.jabref.model.pdf.search.EnglishStemAnalyzer; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; + +public class LuceneTest { + public static void main(String[] args) throws Exception { + // Setup the analyzer + Analyzer analyzer = new EnglishStemAnalyzer(); + + // Store the index in memory + Directory directory = new ByteBuffersDirectory(); + + // Configure index writer + IndexWriterConfig config = new IndexWriterConfig(analyzer); + IndexWriter indexWriter = new IndexWriter(directory, config); + + // Index sample data + String[] texts = {"running", "runner", "ran", "trial", "trials"}; + for (String text : texts) { + Document document = new Document(); + document.add(new TextField("content", text, Field.Store.YES)); + indexWriter.addDocument(document); + } + indexWriter.close(); + + search("trials", directory, analyzer); + } + + public static void search(String queryString, Directory directory, Analyzer analyzer) throws Exception { + Query query = new QueryParser("content", analyzer).parse(queryString); + IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory)); + ScoreDoc[] hits = searcher.search(query, 10).scoreDocs; + + for (ScoreDoc scoreDoc : hits) { + Document doc = searcher.doc(scoreDoc.doc); + System.out.println(doc.get("content")); + } + } +} diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index 4bf01fadb73..c6ce0728860 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -1,243 +1,213 @@ package org.jabref.logic.search; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; import java.util.EnumSet; -import java.util.Optional; -import java.util.regex.Pattern; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.logic.search.indexing.LuceneIndexer; +import org.jabref.logic.search.retrieval.LuceneSearcher; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; 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()); - } - - @Test - public void testIsContainsBasedSearch() { - assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isContainsBasedSearch()); - assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isContainsBasedSearch()); - assertFalse(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isContainsBasedSearch()); - } - - @Test - public void testIsGrammarBasedSearch() { - assertFalse(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isGrammarBasedSearch()); - assertFalse(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isGrammarBasedSearch()); - assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isGrammarBasedSearch()); - } - - @Test - public void testGrammarSearch() { - BibEntry entry = new BibEntry(); - entry.addKeyword("one two", ','); - SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(entry)); - } - - @Test - public void testGrammarSearchFullEntryLastCharMissing() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.TITLE, "systematic revie"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); - assertFalse(searchQuery.isMatch(entry)); - } - - @Test - public void testGrammarSearchFullEntry() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.TITLE, "systematic review"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(entry)); - } - - @Test - public void testSearchingForOpenBraketInBooktitle() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.BOOKTITLE, "Super Conference (SC)"); - - SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(e)); - } - - @Test - public void testSearchMatchesSingleKeywordNotPart() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - - SearchQuery searchQuery = new SearchQuery("anykeyword==apple", EnumSet.noneOf(SearchFlags.class)); - assertFalse(searchQuery.isMatch(e)); - } - - @Test - public void testSearchMatchesSingleKeyword() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - - SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(e)); - } - - @Test - public void testSearchAllFields() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.TITLE, "Fruity features"); - e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - - SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(e)); - } - - @Test - public void testSearchAllFieldsNotForSpecificField() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.TITLE, "Fruity features"); - e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", EnumSet.noneOf(SearchFlags.class)); - assertFalse(searchQuery.isMatch(e)); - } - - @Test - public void testSearchAllFieldsAndSpecificField() { - BibEntry e = new BibEntry(StandardEntryType.InProceedings); - e.setField(StandardField.TITLE, "Fruity features"); - e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", EnumSet.noneOf(SearchFlags.class)); - assertTrue(searchQuery.isMatch(e)); - } - - @Test - public void testIsMatch() { - BibEntry entry = new BibEntry(); - entry.setType(StandardEntryType.Article); - entry.setField(StandardField.AUTHOR, "asdf"); - - assertFalse(new SearchQuery("BiblatexEntryType", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); - assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); - assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); - } - - @Test - public void testIsValidQueryNotAsRegEx() { - assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); - } - - @Test - public void testIsValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); - } - - @Test - public void testIsNotValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryAsRegEx() { - assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryWithNumbersAsRegEx() { - assertTrue(new SearchQuery("123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryContainsBracketAsRegEx() { - assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryWithEqualSignAsRegEx() { - assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryWithNumbersAndEqualSignAsRegEx() { - assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); - } - - @Test - public void testIsValidQueryWithEqualSignNotAsRegEx() { - assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); - } - - @Test - public void testIsValidQueryWithNumbersAndEqualSignNotAsRegEx() { - assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); - } - - @Test - public void isMatchedForNormalAndFieldBasedSearchMixed() { - BibEntry entry = new BibEntry(); - entry.setType(StandardEntryType.Article); - entry.setField(StandardField.AUTHOR, "asdf"); - entry.setField(StandardField.ABSTRACT, "text"); - - assertTrue(new SearchQuery("text AND author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); - } - - @Test - public void testSimpleTerm() { - String query = "progress"; - - SearchQuery result = new SearchQuery(query, EnumSet.noneOf(SearchFlags.class)); - assertFalse(result.isGrammarBasedSearch()); - } - - @Test - public void testGetPattern() { - String query = "progress"; - SearchQuery result = new SearchQuery(query, EnumSet.noneOf(SearchFlags.class)); - Pattern pattern = Pattern.compile("(\\Qprogress\\E)"); - // We can't directly compare the pattern objects - assertEquals(Optional.of(pattern.toString()), result.getPatternForWords().map(Pattern::toString)); - } - - @Test - public void testGetRegexpPattern() { - String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - Pattern pattern = Pattern.compile("([a-c]\\d* \\d*)"); - assertEquals(Optional.of(pattern.toString()), regexQuery.getPatternForWords().map(Pattern::toString)); - } - - @Test - public void testGetRegexpJavascriptPattern() { - String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - Pattern pattern = Pattern.compile("([a-c]\\d* \\d*)"); - assertEquals(Optional.of(pattern.toString()), regexQuery.getJavaScriptPatternForWords().map(Pattern::toString)); - } - - @Test - public void testEscapingInPattern() { - // first word contain all java special regex characters - String queryText = "<([{\\\\^-=$!|]})?*+.> word1 word2."; - SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, EnumSet.noneOf(SearchFlags.class)); - String pattern = "(\\Q<([{\\^-=$!|]})?*+.>\\E)|(\\Qword1\\E)|(\\Qword2.\\E)"; - assertEquals(Optional.of(pattern), textQueryWithSpecialChars.getPatternForWords().map(Pattern::toString)); - } - - @Test - public void testEscapingInJavascriptPattern() { - // first word contain all javascript special regex characters that should be escaped individually in text based search - String queryText = "([{\\\\^$|]})?*+./ word1 word2."; - SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, EnumSet.noneOf(SearchFlags.class)); - String pattern = "(\\(\\[\\{\\\\\\^\\$\\|\\]\\}\\)\\?\\*\\+\\.\\/)|(word1)|(word2\\.)"; - assertEquals(Optional.of(pattern), textQueryWithSpecialChars.getJavaScriptPatternForWords().map(Pattern::toString)); - } + private LuceneSearcher searcher; + private PreferencesService preferencesService; + private BibDatabase bibDatabase; + private BibDatabaseContext bibDatabaseContext; + + @BeforeEach + public void setUp(@TempDir Path indexDir) throws IOException { + preferencesService = mock(PreferencesService.class); + when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); + + bibDatabase = new BibDatabase(); + bibDatabaseContext = mock(BibDatabaseContext.class); + when(bibDatabaseContext.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + when(bibDatabaseContext.getFulltextIndexPath()).thenReturn(indexDir); + when(bibDatabaseContext.getDatabase()).thenReturn(bibDatabase); + when(bibDatabaseContext.getEntries()).thenReturn(bibDatabase.getEntries()); + + LuceneIndexer indexer = LuceneIndexer.of(bibDatabaseContext, preferencesService); + indexer.createIndex(); + + searcher = LuceneSearcher.of(bibDatabaseContext); + } + + @Test + public void nullWhenQueryBlank() { + assertNull(new SearchQuery("", EnumSet.noneOf(SearchRules.SearchFlags.class)).getQuery()); + } + +// +// @Test +// public void testToString() { +// assertEquals("\"asdf\" (case sensitive, regular expression)", new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).toString()); +// assertEquals("\"asdf\" (case insensitive, plain text)", new SearchQuery("asdf", EnumSet.noneOf(SearchRules.SearchFlags.class)).toString()); +// } +// +// @Test +// public void testGrammarSearch() { +// BibEntry entry = new BibEntry(); +// entry.addKeyword("one two", ','); +// SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(entry)); +// } +// +// @Test +// public void testGrammarSearchFullEntryLastCharMissing() { +// BibEntry entry = new BibEntry(); +// entry.setField(StandardField.TITLE, "systematic revie"); +// SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertFalse(searchQuery.isMatch(entry)); +// } +// +// @Test +// public void testGrammarSearchFullEntry() { +// BibEntry entry = new BibEntry(); +// entry.setField(StandardField.TITLE, "systematic review"); +// SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(entry)); +// } +// +// @Test +// public void testSearchingForOpenBraketInBooktitle() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.BOOKTITLE, "Super Conference (SC)"); +// +// SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testSearchMatchesSingleKeywordNotPart() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); +// +// SearchQuery searchQuery = new SearchQuery("anykeyword==apple", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertFalse(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testSearchMatchesSingleKeyword() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); +// +// SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testSearchAllFields() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.TITLE, "Fruity features"); +// e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); +// +// SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testSearchAllFieldsNotForSpecificField() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.TITLE, "Fruity features"); +// e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); +// +// SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertFalse(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testSearchAllFieldsAndSpecificField() { +// BibEntry e = new BibEntry(StandardEntryType.InProceedings); +// e.setField(StandardField.TITLE, "Fruity features"); +// e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); +// +// SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", EnumSet.noneOf(SearchRules.SearchFlags.class)); +// assertTrue(searchQuery.isMatch(e)); +// } +// +// @Test +// public void testIsMatch() { +// BibEntry entry = new BibEntry(); +// entry.setType(StandardEntryType.Article); +// entry.setField(StandardField.AUTHOR, "asdf"); +// +// assertFalse(new SearchQuery("BiblatexEntryType", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); +// assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); +// assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); +// } +// +// @Test +// public void testIsValidQueryNotAsRegEx() { +// assertTrue(new SearchQuery("asdf", EnumSet.noneOf(SearchRules.SearchFlags.class)).isValid()); +// } +// +// @Test +// public void testIsValidQueryContainsBracketNotAsRegEx() { +// assertTrue(new SearchQuery("asdf[", EnumSet.noneOf(SearchRules.SearchFlags.class)).isValid()); +// } +// +// @Test +// public void testIsNotValidQueryContainsBracketNotAsRegEx() { +// assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryAsRegEx() { +// assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryWithNumbersAsRegEx() { +// assertTrue(new SearchQuery("123", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryContainsBracketAsRegEx() { +// assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryWithEqualSignAsRegEx() { +// assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryWithNumbersAndEqualSignAsRegEx() { +// assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); +// } +// +// @Test +// public void testIsValidQueryWithEqualSignNotAsRegEx() { +// assertTrue(new SearchQuery("author=asdf", EnumSet.noneOf(SearchRules.SearchFlags.class)).isValid()); +// } +// +// @Test +// public void testIsValidQueryWithNumbersAndEqualSignNotAsRegEx() { +// assertTrue(new SearchQuery("author=123", EnumSet.noneOf(SearchRules.SearchFlags.class)).isValid()); +// } +// +// @Test +// public void isMatchedForNormalAndFieldBasedSearchMixed() { +// BibEntry entry = new BibEntry(); +// entry.setType(StandardEntryType.Article); +// entry.setField(StandardField.AUTHOR, "asdf"); +// entry.setField(StandardField.ABSTRACT, "text"); +// +// assertTrue(new SearchQuery("text AND author=asdf", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); +// } } diff --git a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java index 8878219ecda..142c2a0fedf 100644 --- a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java +++ b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java @@ -13,7 +13,6 @@ import org.jabref.model.search.matchers.AndMatcher; import org.jabref.model.search.matchers.OrMatcher; import org.jabref.model.search.rules.SearchRules; -import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -84,7 +83,7 @@ private static AbstractGroup getKeywordGroup(String name) { } private static AbstractGroup getSearchGroup(String name) { - return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); + return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.noneOf(SearchRules.SearchFlags.class)); } private static AbstractGroup getExplict(String name) { @@ -256,7 +255,7 @@ void setGroupExplicitToSearchDoesNotKeepPreviousAssignments() { ExplicitGroup oldGroup = new ExplicitGroup("OldGroup", GroupHierarchyType.INDEPENDENT, ','); oldGroup.add(entry); GroupTreeNode node = GroupTreeNode.fromGroup(oldGroup); - AbstractGroup newGroup = new SearchGroup("NewGroup", GroupHierarchyType.INDEPENDENT, "test", EnumSet.noneOf(SearchFlags.class)); + AbstractGroup newGroup = new SearchGroup("NewGroup", GroupHierarchyType.INDEPENDENT, "test", EnumSet.noneOf(SearchRules.SearchFlags.class)); node.setGroup(newGroup, true, true, entries); @@ -334,7 +333,7 @@ void onlySubgroupsContainAllEntries() { @Test void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.noneOf(SearchRules.SearchFlags.class))); List fieldChanges = searchGroup.addEntriesToGroup(entries); assertEquals(Collections.emptyList(), fieldChanges); @@ -342,7 +341,7 @@ void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { @Test void removeEntriesFromGroupWorksNotForGroupsNotSupportingExplicitRemovalOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.noneOf(SearchRules.SearchFlags.class))); List fieldChanges = searchGroup.removeEntriesFromGroup(entries); assertEquals(Collections.emptyList(), fieldChanges); diff --git a/src/test/java/org/jabref/model/groups/SearchGroupTest.java b/src/test/java/org/jabref/model/groups/SearchGroupTest.java index 5bdec41d87e..1e7ecbdb4de 100644 --- a/src/test/java/org/jabref/model/groups/SearchGroupTest.java +++ b/src/test/java/org/jabref/model/groups/SearchGroupTest.java @@ -15,7 +15,7 @@ public class SearchGroupTest { @Test public void containsFindsWordWithRegularExpression() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); BibEntry entry = new BibEntry(); entry.addKeyword("review", ','); @@ -24,7 +24,7 @@ public void containsFindsWordWithRegularExpression() { @Test public void containsDoesNotFindsWordWithInvalidRegularExpression() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=*rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=*rev*", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); BibEntry entry = new BibEntry(); entry.addKeyword("review", ','); diff --git a/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java deleted file mode 100644 index ed77afd40be..00000000000 --- a/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.EnumSet; - -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Test case for ContainBasedSearchRule. - */ -public class ContainsBasedSearchRuleTest { - - private final BibEntry be = new BibEntry(StandardEntryType.InCollection) - .withCitationKey("shields01") - .withField(StandardField.TITLE, "Marine finfish larviculture in Europe") - .withField(StandardField.YEAR, "2001") - .withField(StandardField.AUTHOR, "Kevin Shields"); - private final ContainsBasedSearchRule bsCaseSensitive = new ContainsBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - private final ContainsBasedSearchRule bsCaseInsensitive = new ContainsBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - private final RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - private final RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - @Test - public void testContentOfSingleField() { - String query = "\"marine larviculture\""; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertFalse(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); - } - - @Test - public void testContentDistributedOnMultipleFields() { - String query = "marine 2001 shields"; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertTrue(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); - } - - @Test - public void testRegularExpressionMatch() { - String query = "marine [A-Za-z]* larviculture"; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertFalse(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertTrue(bsCaseInsensitiveRegexp.applyRule(query, be)); - } -} diff --git a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java deleted file mode 100644 index 6b410b5a088..00000000000 --- a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.EnumSet; - -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Test case for GrammarBasedSearchRuleTest. - */ -public class GrammarBasedSearchRuleTest { - - @Test - void applyRuleMatchesSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "M[a-z]+e"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, makeBibtexEntry())); - } - - @Test - void applyRuleDoesNotMatchSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "M[0-9]+e"; - assertTrue(searchRule.validateSearchStrings(query)); - assertFalse(searchRule.applyRule(query, makeBibtexEntry())); - } - - @Test - void searchRuleOfDocumentationMatches() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "(author = miller or title|keywords = \"image processing\") and not author = brown"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, new BibEntry() - .withCitationKey("key") - .withField(StandardField.KEYWORDS, "image processing"))); - assertFalse(searchRule.applyRule(query, new BibEntry() - .withCitationKey("key") - .withField(StandardField.AUTHOR, "Sam Brown") - .withField(StandardField.KEYWORDS, "image processing"))); - } - - @Disabled - @Test - void searchForAnyFieldWorks() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "anyfield:fruit"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, new BibEntry() - .withField(StandardField.KEYWORDS, "fruit"))); - } - - @Disabled - @Test - void searchForAnyKeywordWorks() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "anykeyword:apple"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, new BibEntry() - .withField(StandardField.KEYWORDS, "apple"))); - assertFalse(searchRule.applyRule(query, new BibEntry() - .withField(StandardField.KEYWORDS, "pineapple"))); - } - - @Test - void searchForCitationKeyWorks() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.noneOf(SearchRules.SearchFlags.class)); - String query = "citationkey==miller2005"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, new BibEntry() - .withCitationKey("miller2005"))); - } - - @Test - void searchForThesisEntryTypeWorks() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.noneOf(SearchRules.SearchFlags.class)); - String query = "entrytype=thesis"; - assertTrue(searchRule.validateSearchStrings(query)); - assertTrue(searchRule.applyRule(query, new BibEntry(StandardEntryType.PhdThesis))); - } - - public BibEntry makeBibtexEntry() { - return new BibEntry(StandardEntryType.InCollection) - .withCitationKey("shields01") - .withField(StandardField.TITLE, "Marine finfish larviculture in Europe") - .withField(StandardField.YEAR, "2001") - .withField(StandardField.AUTHOR, "Kevin Shields"); - } -} diff --git a/src/test/java/org/jabref/model/search/rules/SentenceAnalyzerTest.java b/src/test/java/org/jabref/model/search/rules/SentenceAnalyzerTest.java deleted file mode 100644 index 61e05e3def4..00000000000 --- a/src/test/java/org/jabref/model/search/rules/SentenceAnalyzerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.List; -import java.util.stream.Stream; - -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; - -public class SentenceAnalyzerTest { - - static Stream getParameters() { - return Stream.of( - Arguments.of(List.of("a", "b"), "a b"), - - // Leading and trailing spaces - Arguments.of(List.of("a", "b"), " a b "), - - // Escaped characters and trailing spaces - Arguments.of(List.of("b "), "\"b \" "), - - // Escaped characters and leading spaces. - Arguments.of(List.of(" a"), " \\ a") - ); - } - - @ParameterizedTest - @MethodSource("getParameters") - public void testGetWords(List expected, String input) { - assertEquals(expected, new SentenceAnalyzer(input).getWords()); - } -}