diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fee3b13094..4efbdad5a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Added +- We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838) + ### Changed ### Fixed diff --git a/build.gradle b/build.gradle index 976b8e468cd..c8c9d165e5d 100644 --- a/build.gradle +++ b/build.gradle @@ -142,10 +142,6 @@ dependencies { antlr4 'org.antlr:antlr4:4.9.2' implementation 'org.antlr:antlr4-runtime:4.9.2' - implementation (group: 'org.apache.lucene', name: 'lucene-queryparser', version: '8.9.0') { - exclude group: 'org.apache.lucene', module: 'lucene-sandbox' - } - implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '5.12.0.202106070339-r' implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.12.4' @@ -209,6 +205,8 @@ dependencies { implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2' implementation 'com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.62.2' + implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' + testImplementation 'io.github.classgraph:classgraph:4.8.110' testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2' diff --git a/external-libraries.md b/external-libraries.md index 769fc47c35f..add09f3b076 100644 --- a/external-libraries.md +++ b/external-libraries.md @@ -310,7 +310,7 @@ License: Apache-2.0 ``` ```yaml -Id: org.apache.lucene:lucene-ueryparser +Id: org.apache.lucene:lucene-queryparser Project: Apache Lucene URL: https://lucene.apache.org/ License: Apache-2.0 @@ -567,6 +567,9 @@ org.apache.logging.log4j:log4j-slf4j18-impl:3.0.0-SNAPSHOT org.apache.lucene:lucene-core:8.9.0 org.apache.lucene:lucene-queries:8.9.0 org.apache.lucene:lucene-queryparser:8.9.0 +org.apache.lucene:lucene-analyzers-common:8.9.0 +org.apache.lucene:lucene-backward-codecs:8.9.0 +org.apache.lucene:lucene-highlighter:8.9.0 org.apache.pdfbox:fontbox:2.0.24 org.apache.pdfbox:pdfbox:2.0.24 org.apache.pdfbox:xmpbox:2.0.24 diff --git a/lib/lucene.jar b/lib/lucene.jar new file mode 100644 index 00000000000..758012cdc90 Binary files /dev/null and b/lib/lucene.jar differ diff --git a/lucene-jar/lib/build.gradle b/lucene-jar/lib/build.gradle new file mode 100644 index 00000000000..b267c5c7e45 --- /dev/null +++ b/lucene-jar/lib/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' + id 'com.github.johnrengelman.shadow' version '7.0.0' +} + +repositories { + mavenCentral() +} + +shadowJar { + mergeServiceFiles() +} + +dependencies { + implementation 'org.apache.lucene:lucene-core:8.9.0' + implementation ('org.apache.lucene:lucene-queryparser:8.9.0') { + exclude module: "lucene-sandbox" + } + implementation 'org.apache.lucene:lucene-queries:8.9.0' + implementation 'org.apache.lucene:lucene-analyzers-common:8.9.0' + implementation 'org.apache.lucene:lucene-backward-codecs:8.9.0' + implementation 'org.apache.lucene:lucene-highlighter:8.9.0' +} diff --git a/lucene-jar/settings.gradle b/lucene-jar/settings.gradle new file mode 100644 index 00000000000..d810394f855 --- /dev/null +++ b/lucene-jar/settings.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.0.2/userguide/multi_project_builds.html + */ + +rootProject.name = 'lucene-jar' +include('lib') diff --git a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java index 31935438c71..bfc43d706da 100644 --- a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java +++ b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java @@ -3,6 +3,7 @@ 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; @@ -28,6 +29,7 @@ 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.model.util.DummyFileUpdateMonitor; import org.jabref.preferences.JabRefPreferences; @@ -93,14 +95,14 @@ public String write() throws Exception { @Benchmark public List search() { // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", false, false); + SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); return database.getEntries().stream().filter(searchQuery::isMatch).collect(Collectors.toList()); } @Benchmark public List parallelSearch() { // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", false, false); + SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); return database.getEntries().parallelStream().filter(searchQuery::isMatch).collect(Collectors.toList()); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f8eba5b2f48..95524dc31b2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -95,10 +95,10 @@ requires flexmark.util.ast; requires flexmark.util.data; requires com.h2database.mvstore; - requires lucene.queryparser; - requires lucene.core; + requires lucene; requires org.eclipse.jgit; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.dataformat.yaml; requires com.fasterxml.jackson.datatype.jsr310; + requires net.harawata.appdirs; } diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index 50832d464ae..f30aa0cafaf 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -343,8 +343,7 @@ private boolean exportMatches(List loaded) { BibDatabase dataBase = pr.getDatabase(); SearchPreferences searchPreferences = Globals.prefs.getSearchPreferences(); - SearchQuery query = new SearchQuery(searchTerm, searchPreferences.isCaseSensitive(), - searchPreferences.isRegularExpression()); + SearchQuery query = new SearchQuery(searchTerm, searchPreferences.getSearchFlags()); List matches = new DatabaseSearcher(query, dataBase).getMatches(); // export matches diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 817fd6263bb..6361fd9d76e 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -106,6 +106,7 @@ import org.jabref.gui.push.PushToApplicationAction; import org.jabref.gui.push.PushToApplicationsManager; import org.jabref.gui.search.GlobalSearchBar; +import org.jabref.gui.search.RebuildFulltextSearchIndexAction; import org.jabref.gui.shared.ConnectToSharedDatabaseCommand; import org.jabref.gui.shared.PullChangesFromSharedAction; import org.jabref.gui.slr.ExistingStudySearchAction; @@ -819,7 +820,11 @@ private MenuBar createMenu() { pushToApplicationMenuItem, new SeparatorMenuItem(), factory.createMenuItem(StandardActions.START_NEW_STUDY, new StartNewStudyAction(this, Globals.getFileUpdateMonitor(), Globals.TASK_EXECUTOR, prefs)), - factory.createMenuItem(StandardActions.SEARCH_FOR_EXISTING_STUDY, new ExistingStudySearchAction(this, Globals.getFileUpdateMonitor(), Globals.TASK_EXECUTOR, prefs)) + factory.createMenuItem(StandardActions.SEARCH_FOR_EXISTING_STUDY, new ExistingStudySearchAction(this, Globals.getFileUpdateMonitor(), Globals.TASK_EXECUTOR, prefs)), + + new SeparatorMenuItem(), + + factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, this::getCurrentLibraryTab, dialogService, prefs.getFilePreferences())) ); SidePaneComponent webSearch = sidePaneManager.getComponent(SidePaneType.WEB_SEARCH); diff --git a/src/main/java/org/jabref/gui/JabRefMain.java b/src/main/java/org/jabref/gui/JabRefMain.java index ae0a6acbf96..11636af520a 100644 --- a/src/main/java/org/jabref/gui/JabRefMain.java +++ b/src/main/java/org/jabref/gui/JabRefMain.java @@ -1,6 +1,12 @@ package org.jabref.gui; +import java.io.File; +import java.io.IOException; import java.net.Authenticator; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; import javafx.application.Application; import javafx.application.Platform; @@ -20,6 +26,7 @@ import org.jabref.logic.remote.client.RemoteClient; import org.jabref.logic.util.OS; import org.jabref.migrations.PreferencesMigrations; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.preferences.JabRefPreferences; import org.jabref.preferences.PreferencesService; @@ -59,6 +66,8 @@ public void start(Stage mainStage) { applyPreferences(preferences); + clearOldSearchIndices(); + try { // Process arguments ArgumentProcessor argumentProcessor = new ArgumentProcessor(arguments, ArgumentProcessor.Mode.INITIAL_START); @@ -139,4 +148,24 @@ private static void configureProxy(ProxyPreferences proxyPreferences) { Authenticator.setDefault(new ProxyAuthenticator()); } } + + private static void clearOldSearchIndices() { + Path currentIndexPath = BibDatabaseContext.getFulltextIndexBasePath(); + Path appData = currentIndexPath.getParent(); + + try (DirectoryStream stream = Files.newDirectoryStream(appData)) { + for (Path path : stream) { + if (Files.isDirectory(path) && !path.equals(currentIndexPath)) { + LOGGER.info("Deleting out-of-date fulltext search index at {}.", path); + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + + } + } + } catch (IOException e) { + LOGGER.error("Could not access app-directory at {}", appData, e); + } + } } diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index eef077f6680..c6c55f4681b 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -1,6 +1,7 @@ package org.jabref.gui; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -43,8 +44,11 @@ import org.jabref.logic.autosaveandbackup.BackupManager; import org.jabref.logic.citationstyle.CitationStyleCache; import org.jabref.logic.importer.ParserResult; +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.shared.DatabaseLocation; import org.jabref.logic.util.UpdateField; @@ -56,10 +60,13 @@ import org.jabref.model.database.event.EntriesAddedEvent; import org.jabref.model.database.event.EntriesRemovedEvent; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; +import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; import org.jabref.preferences.PreferencesService; import com.google.common.eventbus.Subscribe; @@ -101,6 +108,8 @@ public class LibraryTab extends Tab { // initializing it so we prevent NullPointerException private BackgroundTask dataLoadingTask = BackgroundTask.wrap(() -> null); + private IndexingTaskManager indexingTaskManager = new IndexingTaskManager(Globals.TASK_EXECUTOR); + public LibraryTab(JabRefFrame frame, PreferencesService preferencesService, BibDatabaseContext bibDatabaseContext, @@ -125,6 +134,7 @@ public LibraryTab(JabRefFrame frame, setupAutoCompletion(); this.getDatabase().registerListener(new SearchListener()); + this.getDatabase().registerListener(new IndexUpdateListener()); this.getDatabase().registerListener(new EntriesRemovedListener()); // ensure that at each addition of a new entry, the entry is added to the groups interface @@ -332,6 +342,8 @@ public void updateTabTitle(boolean isChanged) { textProperty().setValue(tabTitle.toString()); setTooltip(new Tooltip(toolTipText.toString())); }); + + indexingTaskManager.updateDatabaseName(tabTitle.toString()); } private List collectAllDatabasePaths() { @@ -846,4 +858,63 @@ public void listen(EntriesRemovedEvent removedEntriesEvent) { DefaultTaskExecutor.runInJavaFXThread(() -> frame.getGlobalSearchBar().performSearch()); } } + + private class IndexUpdateListener { + + public IndexUpdateListener() { + try { + indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); + } + } + + @Subscribe + public void listen(EntriesAddedEvent addedEntryEvent) { + 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); + } + } + + @Subscribe + public void listen(EntriesRemovedEvent removedEntriesEvent) { + 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); + } + } + + @Subscribe + public void listen(FieldChangedEvent fieldChangedEvent) { + 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); + } + } + } + } + + public IndexingTaskManager getIndexingTaskManager() { + return indexingTaskManager; + } } diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index b01b7dfdb59..4519e5b3f4a 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -30,6 +30,7 @@ public enum StandardActions implements Action { DELETE(Localization.lang("Delete"), IconTheme.JabRefIcons.DELETE_ENTRY), DELETE_ENTRY(Localization.lang("Delete Entry"), IconTheme.JabRefIcons.DELETE_ENTRY, KeyBinding.DELETE_ENTRY), SEND_AS_EMAIL(Localization.lang("Send as email"), IconTheme.JabRefIcons.EMAIL), + REBUILD_FULLTEXT_SEARCH_INDEX(Localization.lang("Rebuild fulltext search index"), IconTheme.JabRefIcons.FILE), OPEN_EXTERNAL_FILE(Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 7665da90056..84c98978b75 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -31,6 +31,7 @@ import org.jabref.gui.StateManager; import org.jabref.gui.citationkeypattern.GenerateCitationKeySingleAction; import org.jabref.gui.entryeditor.fileannotationtab.FileAnnotationTab; +import org.jabref.gui.entryeditor.fileannotationtab.FulltextSearchResultsTab; import org.jabref.gui.externalfiles.ExternalFilesEntryLinker; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.help.HelpAction; @@ -267,6 +268,8 @@ private List createTabs() { // LaTeX citations tab entryEditorTabs.add(new LatexCitationsTab(databaseContext, preferencesService, taskExecutor, dialogService)); + entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService.getTheme(), preferencesService.getFilePreferences())); + return entryEditorTabs; } diff --git a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java new file mode 100644 index 00000000000..dc83f662a8e --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java @@ -0,0 +1,82 @@ +package org.jabref.gui.entryeditor.fileannotationtab; + +import java.nio.file.Path; + +import javafx.scene.web.WebView; + +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.EntryEditorTab; +import org.jabref.gui.util.OpenHyperlinksInExternalBrowser; +import org.jabref.gui.util.Theme; +import org.jabref.logic.l10n.Localization; +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.SearchResult; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.preferences.FilePreferences; + +public class FulltextSearchResultsTab extends EntryEditorTab { + + private final StateManager stateManager; + private final FilePreferences filePreferences; + + private final WebView webView; + + public FulltextSearchResultsTab(StateManager stateManager, Theme theme, FilePreferences filePreferences) { + this.stateManager = stateManager; + this.filePreferences = filePreferences; + webView = new WebView(); + setTheme(theme); + webView.getEngine().loadContent(wrapHTML("

" + Localization.lang("Search results") + "

")); + setContent(webView); + webView.getEngine().getLoadWorker().stateProperty().addListener(new OpenHyperlinksInExternalBrowser(webView)); + setText(Localization.lang("Search results")); + } + + @Override + 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; + } + + @Override + protected void bindToEntry(BibEntry entry) { + if (!shouldShow(entry)) { + return; + } + PdfSearchResults searchResults = stateManager.activeSearchQueryProperty().get().get().getRule().getFulltextResults(stateManager.activeSearchQueryProperty().get().get().getQuery(), entry); + StringBuilder content = new StringBuilder(); + + content.append("

"); + if (searchResults.numSearchResults() == 0) { + content.append(Localization.lang("No search matches.")); + } else { + content.append(Localization.lang("Search results")); + } + content.append("

"); + + for (SearchResult searchResult : searchResults.getSearchResults()) { + content.append("

"); + LinkedFile linkedFile = new LinkedFile("just for link", Path.of(searchResult.getPath()), "pdf"); + Path resolvedPath = linkedFile.findIn(stateManager.getActiveDatabase().get(), filePreferences).orElse(Path.of(searchResult.getPath())); + String link = "" + searchResult.getPath() + ""; + content.append(Localization.lang("Found match in %0", link)); + content.append("

"); + content.append(searchResult.getHtml()); + content.append("

"); + } + + webView.getEngine().loadContent(wrapHTML(content.toString())); + } + + private String wrapHTML(String content) { + return "
" + content + "
"; + } + + public void setTheme(Theme theme) { + theme.getAdditionalStylesheet().ifPresent(location -> webView.getEngine().setUserStyleSheetLocation(location)); + } +} diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/src/main/java/org/jabref/gui/groups/GroupDialogView.java index eaa758de519..e1e74ad1b85 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogView.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogView.java @@ -1,6 +1,7 @@ package org.jabref.gui.groups; import java.util.EnumMap; +import java.util.EnumSet; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -21,6 +22,8 @@ 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.preferences.PreferencesService; import com.airhacks.afterburner.views.ViewLoader; @@ -124,8 +127,24 @@ public void initialize() { keywordGroupRegex.selectedProperty().bindBidirectional(viewModel.keywordGroupRegexProperty()); searchGroupSearchTerm.textProperty().bindBidirectional(viewModel.searchGroupSearchTermProperty()); - searchGroupCaseSensitive.selectedProperty().bindBidirectional(viewModel.searchGroupCaseSensitiveProperty()); - searchGroupRegex.selectedProperty().bindBidirectional(viewModel.searchGroupRegexProperty()); + 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.selectedProperty().addListener((observable, oldValue, newValue) -> { + EnumSet searchFlags = viewModel.searchFlagsProperty().get(); + if (newValue) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } else { + searchFlags.remove(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } + viewModel.searchFlagsProperty().set(searchFlags); + }); autoGroupKeywordsOption.selectedProperty().bindBidirectional(viewModel.autoGroupKeywordsOptionProperty()); autoGroupKeywordsField.textProperty().bindBidirectional(viewModel.autoGroupKeywordsFieldProperty()); diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java index ebd95f3a8f1..1d5fcc2a79d 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @@ -48,6 +49,8 @@ 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.preferences.PreferencesService; @@ -80,8 +83,7 @@ public class GroupDialogViewModel { private final BooleanProperty keywordGroupRegexProperty = new SimpleBooleanProperty(); private final StringProperty searchGroupSearchTermProperty = new SimpleStringProperty(""); - private final BooleanProperty searchGroupCaseSensitiveProperty = new SimpleBooleanProperty(); - private final BooleanProperty searchGroupRegexProperty = new SimpleBooleanProperty(); + private final ObjectProperty> searchFlagsProperty = new SimpleObjectProperty<>(); private final BooleanProperty autoGroupKeywordsOptionProperty = new SimpleBooleanProperty(); private final StringProperty autoGroupKeywordsFieldProperty = new SimpleStringProperty(""); @@ -197,7 +199,7 @@ private void setupValidation() { searchRegexValidator = new FunctionBasedValidator<>( searchGroupSearchTermProperty, input -> { - if (!searchGroupRegexProperty.getValue()) { + if (!searchFlagsProperty.getValue().contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return true; } @@ -310,8 +312,7 @@ public AbstractGroup resultConverter(ButtonType button) { groupName, groupHierarchySelectedProperty.getValue(), searchGroupSearchTermProperty.getValue().trim(), - searchGroupCaseSensitiveProperty.getValue(), - searchGroupRegexProperty.getValue()); + searchFlagsProperty.getValue()); } else if (typeAutoProperty.getValue()) { if (autoGroupKeywordsOptionProperty.getValue()) { // Set default value for delimiters: ',' for base and '>' for hierarchical @@ -396,8 +397,7 @@ public void setValues() { SearchGroup group = (SearchGroup) editedGroup; searchGroupSearchTermProperty.setValue(group.getSearchExpression()); - searchGroupCaseSensitiveProperty.setValue(group.isCaseSensitive()); - searchGroupRegexProperty.setValue(group.isRegularExpression()); + searchFlagsProperty.setValue(group.getSearchFlags()); } else if (editedGroup.getClass() == ExplicitGroup.class) { typeExplicitProperty.setValue(true); } else if (editedGroup instanceof AutomaticGroup) { @@ -548,12 +548,8 @@ public StringProperty searchGroupSearchTermProperty() { return searchGroupSearchTermProperty; } - public BooleanProperty searchGroupCaseSensitiveProperty() { - return searchGroupCaseSensitiveProperty; - } - - public BooleanProperty searchGroupRegexProperty() { - return searchGroupRegexProperty; + public ObjectProperty> searchFlagsProperty() { + return searchFlagsProperty; } public BooleanProperty autoGroupKeywordsOptionProperty() { diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index 7389e233c04..e3e9aab7b23 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -279,6 +279,7 @@ public enum JabRefIcons implements JabRefIcon { ERROR(MaterialDesignA.ALERT_CIRCLE), CASE_SENSITIVE(MaterialDesignA.ALPHABETICAL), REG_EX(MaterialDesignR.REGEX), + FULLTEXT(MaterialDesignF.FILE_EYE), CONSOLE(MaterialDesignC.CONSOLE), FORUM(MaterialDesignF.FORUM), FACEBOOK(MaterialDesignF.FACEBOOK), diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 230a6c83f7c..2f19d45bb50 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -80,6 +80,7 @@ public class GlobalSearchBar extends HBox { private final CustomTextField searchField = SearchTextField.create(); private final ToggleButton caseSensitiveButton; private final ToggleButton regularExpressionButton; + private final ToggleButton fulltextButton; // private final Button searchModeButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); @@ -123,12 +124,14 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regularExpressionButton = IconTheme.JabRefIcons.REG_EX.asToggleButton(); caseSensitiveButton = IconTheme.JabRefIcons.CASE_SENSITIVE.asToggleButton(); + fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); // searchModeButton = new Button(); initSearchModifierButtons(); BooleanBinding focusedOrActive = searchField.focusedProperty() .or(regularExpressionButton.focusedProperty()) .or(caseSensitiveButton.focusedProperty()) + .or(fulltextButton.focusedProperty()) .or(searchField.textProperty() .isNotEmpty()); @@ -136,8 +139,10 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regularExpressionButton.visibleProperty().bind(focusedOrActive); caseSensitiveButton.visibleProperty().unbind(); caseSensitiveButton.visibleProperty().bind(focusedOrActive); + fulltextButton.visibleProperty().unbind(); + fulltextButton.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton)); + StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton)); modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); @@ -195,6 +200,15 @@ private void initSearchModifierButtons() { performSearch(); }); + fulltextButton.setSelected(searchPreferences.isFulltext()); + fulltextButton.setTooltip(new Tooltip(Localization.lang("Fulltext search"))); + initSearchModifierButton(fulltextButton); + fulltextButton.setOnAction(event -> { + searchPreferences = searchPreferences.withFulltext(fulltextButton.isSelected()); + preferencesService.storeSearchPreferences(searchPreferences); + performSearch(); + }); + // ToDo: Reimplement searchMode (searchModeButton) /* searchModeButton.setText(searchPreferences.getSearchDisplayMode().getDisplayName()); searchModeButton.setTooltip(new Tooltip(searchPreferences.getSearchDisplayMode().getToolTipText())); @@ -251,7 +265,7 @@ public void performSearch() { return; } - SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), searchPreferences.isCaseSensitive(), searchPreferences.isRegularExpression()); + SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), searchPreferences.getSearchFlags()); if (!searchQuery.isValid()) { informUserAboutInvalidSearchQuery(); return; diff --git a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java new file mode 100644 index 00000000000..f103ed07a8d --- /dev/null +++ b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java @@ -0,0 +1,81 @@ +package org.jabref.gui.search; + +import java.io.IOException; + +import org.jabref.gui.DialogService; +import org.jabref.gui.Globals; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.FilePreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.gui.actions.ActionHelper.needsDatabase; + +public class RebuildFulltextSearchIndexAction extends SimpleCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final StateManager stateManager; + private final GetCurrentLibraryTab currentLibraryTab; + private final DialogService dialogService; + private final FilePreferences filePreferences; + + private BibDatabaseContext databaseContext; + + private boolean shouldContinue = true; + + public RebuildFulltextSearchIndexAction(StateManager stateManager, GetCurrentLibraryTab currentLibraryTab, DialogService dialogService, FilePreferences filePreferences) { + this.stateManager = stateManager; + this.currentLibraryTab = currentLibraryTab; + this.dialogService = dialogService; + this.filePreferences = filePreferences; + + this.executable.bind(needsDatabase(stateManager)); + } + + @Override + public void execute() { + init(); + BackgroundTask.wrap(this::rebuildIndex) + .executeWith(Globals.TASK_EXECUTOR); + } + + public void init() { + if (stateManager.getActiveDatabase().isEmpty()) { + return; + } + + databaseContext = stateManager.getActiveDatabase().get(); + boolean confirm = dialogService.showConfirmationDialogAndWait( + Localization.lang("Rebuild fulltext search index"), + Localization.lang("Rebuild fulltext search index for current library?")); + if (!confirm) { + shouldContinue = false; + return; + } + dialogService.notify(Localization.lang("Rebuilding fulltext search index...")); + } + + private void rebuildIndex() { + if (!shouldContinue || stateManager.getActiveDatabase().isEmpty()) { + return; + } + try { + currentLibraryTab.get().getIndexingTaskManager().createIndex(PdfIndexer.of(databaseContext, filePreferences), databaseContext.getDatabase(), databaseContext); + } catch (IOException e) { + dialogService.notify(Localization.lang("Failed to access fulltext search index")); + LOGGER.error("Failed to access fulltext search index", e); + } + } + + public interface GetCurrentLibraryTab { + LibraryTab get(); + } +} 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 index 4c65d5dd9d4..d411c263c7c 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java @@ -1,5 +1,6 @@ package org.jabref.gui.search.rules.describer; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -7,17 +8,17 @@ 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 boolean regExp; - private final boolean caseSensitive; + private final EnumSet searchFlags; private final String query; - public ContainsAndRegexBasedSearchRuleDescriber(boolean caseSensitive, boolean regExp, String query) { - this.caseSensitive = caseSensitive; - this.regExp = regExp; + public ContainsAndRegexBasedSearchRuleDescriber(EnumSet searchFlags, String query) { + this.searchFlags = searchFlags; this.query = query; } @@ -26,7 +27,7 @@ public TextFlow getDescription() { List words = new SentenceAnalyzer(query).getWords(); String firstWord = words.isEmpty() ? "" : words.get(0); - String temp = regExp ? Localization.lang( + 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)); @@ -47,7 +48,7 @@ public TextFlow getDescription() { } private Text getCaseSensitiveDescription() { - if (caseSensitive) { + 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 index 8b193d12a1b..416aa8dd618 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java @@ -1,6 +1,7 @@ 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; @@ -12,6 +13,8 @@ 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; @@ -20,13 +23,11 @@ public class GrammarBasedSearchRuleDescriber implements SearchDescriber { - private final boolean caseSensitive; - private final boolean regExp; + private final EnumSet searchFlags; private final ParseTree parseTree; - public GrammarBasedSearchRuleDescriber(boolean caseSensitive, boolean regExp, ParseTree parseTree) { - this.caseSensitive = caseSensitive; - this.regExp = regExp; + public GrammarBasedSearchRuleDescriber(EnumSet searchFlags, ParseTree parseTree) { + this.searchFlags = searchFlags; this.parseTree = Objects.requireNonNull(parseTree); } @@ -39,7 +40,7 @@ public TextFlow getDescription() { 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(caseSensitive ? Localization + 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; @@ -84,7 +85,7 @@ public List visitComparison(SearchParser.ComparisonContext context) { final Optional fieldDescriptor = Optional.ofNullable(context.left); final String value = StringUtil.unquote(context.right.getText(), '"'); if (!fieldDescriptor.isPresent()) { - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(caseSensitive, regExp, value).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(searchFlags, value).getDescription(); description.getChildren().forEach(it -> textList.add((Text) it)); return textList; } @@ -97,19 +98,19 @@ public List visitComparison(SearchParser.ComparisonContext context) { "any field that matches the regular expression %0") : Localization.lang("the field %0"); if (operator == GrammarBasedSearchRule.ComparisonOperator.CONTAINS) { - if (regExp) { + 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 (regExp) { + 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 (regExp) { + 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); 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 index 35424d84398..4cc5e2ebd82 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java @@ -17,18 +17,12 @@ private SearchDescribers() { * @return the search describer to turn the search into something human understandable */ public static SearchDescriber getSearchDescriberFor(SearchQuery searchQuery) { - if (searchQuery.getRule() instanceof GrammarBasedSearchRule) { - GrammarBasedSearchRule grammarBasedSearchRule = (GrammarBasedSearchRule) searchQuery.getRule(); - - return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.isCaseSensitiveSearch(), grammarBasedSearchRule.isRegExpSearch(), grammarBasedSearchRule.getTree()); - } else if (searchQuery.getRule() instanceof ContainBasedSearchRule) { - ContainBasedSearchRule containBasedSearchRule = (ContainBasedSearchRule) searchQuery.getRule(); - - return new ContainsAndRegexBasedSearchRuleDescriber(containBasedSearchRule.isCaseSensitive(), false, searchQuery.getQuery()); - } else if (searchQuery.getRule() instanceof RegexBasedSearchRule) { - RegexBasedSearchRule regexBasedSearchRule = (RegexBasedSearchRule) searchQuery.getRule(); - - return new ContainsAndRegexBasedSearchRuleDescriber(regexBasedSearchRule.isCaseSensitive(), true, searchQuery.getQuery()); + if (searchQuery.getRule() instanceof GrammarBasedSearchRule grammarBasedSearchRule) { + return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.getSearchFlags(), grammarBasedSearchRule.getTree()); + } else if (searchQuery.getRule() instanceof ContainBasedSearchRule 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/util/BackgroundTask.java b/src/main/java/org/jabref/gui/util/BackgroundTask.java index 9c9626354d1..280df1bc0ad 100644 --- a/src/main/java/org/jabref/gui/util/BackgroundTask.java +++ b/src/main/java/org/jabref/gui/util/BackgroundTask.java @@ -64,7 +64,7 @@ protected V call() throws Exception { public static BackgroundTask wrap(Runnable runnable) { return new BackgroundTask<>() { @Override - protected Void call() throws Exception { + protected Void call() { runnable.run(); return null; } diff --git a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java index 635532c2f46..4e9c9997dd8 100644 --- a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java +++ b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java @@ -18,6 +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.strings.StringUtil; public class GroupSerializer { @@ -69,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.isCaseSensitive())); + sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.SearchFlags.CASE_SENSITIVE))); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); - sb.append(StringUtil.booleanToBinaryString(group.isRegularExpression())); + sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.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 9ce459a4829..0fc85c6b03a 100644 --- a/src/main/java/org/jabref/logic/importer/util/GroupsParser.java +++ b/src/main/java/org/jabref/logic/importer/util/GroupsParser.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.List; import org.jabref.logic.auxparser.DefaultAuxParser; @@ -26,6 +27,8 @@ 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; @@ -274,12 +277,17 @@ private static AbstractGroup searchGroupFromString(String s) { String name = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR); int context = Integer.parseInt(tok.nextToken()); String expression = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR); - boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1; - boolean regExp = Integer.parseInt(tok.nextToken()) == 1; + EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); + if (Integer.parseInt(tok.nextToken()) == 1) { + searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); + } + if (Integer.parseInt(tok.nextToken()) == 1) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } // version 0 contained 4 additional booleans to specify search // fields; these are ignored now, all fields are always searched SearchGroup searchGroup = new SearchGroup(name, - GroupHierarchyType.getByNumberOrDefault(context), expression, caseSensitive, regExp + GroupHierarchyType.getByNumberOrDefault(context), expression, searchFlags ); addGroupDetails(tok, searchGroup); return searchGroup; diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java b/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java new file mode 100644 index 00000000000..00e1ee0ccb3 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java @@ -0,0 +1,146 @@ +package org.jabref.logic.pdf.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.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.jabref.gui.LibraryTab; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.strings.StringUtil; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.text.PDFTextStripper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.model.pdf.search.SearchFieldConstants.ANNOTATIONS; +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.PATH; + +/** + * Utility class for reading the data from LinkedFiles of a BibEntry for Lucene. + */ +public final class DocumentReader { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final BibEntry entry; + private final FilePreferences filePreferences; + + /** + * Creates a new DocumentReader using a BibEntry. + * + * @param bibEntry Must not be null and must have at least one LinkedFile. + */ + public DocumentReader(BibEntry bibEntry, FilePreferences filePreferences) { + this.filePreferences = filePreferences; + if (bibEntry.getFiles().isEmpty()) { + throw new IllegalStateException("There are no linked PDF files to this BibEntry!"); + } + + this.entry = bibEntry; + } + + /** + * Reads a LinkedFile of a BibEntry and converts it into a Lucene Document which is then returned. + * + * @return An Optional of a Lucene Document with the (meta)data. Can be empty if there is a problem reading the LinkedFile. + */ + public Optional readLinkedPdf(BibDatabaseContext databaseContext, LinkedFile pdf) { + Optional pdfPath = pdf.findIn(databaseContext, filePreferences); + if (pdfPath.isPresent()) { + try { + return Optional.of(readPdfContents(pdf, pdfPath.get())); + } catch (IOException e) { + LOGGER.error("Could not read pdf file {}!", pdf.getLink(), e); + } + } + return Optional.empty(); + } + + /** + * Reads each LinkedFile of a BibEntry and converts them into Lucene Documents which are then returned. + * + * @return A List of Documents with the (meta)data. Can be empty if there is a problem reading the LinkedFile. + */ + public List readLinkedPdfs(BibDatabaseContext databaseContext) { + return entry.getFiles().stream() + .map((pdf) -> readLinkedPdf(databaseContext, pdf)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + private Document readPdfContents(LinkedFile pdf, Path resolvedPdfPath) throws IOException { + try (PDDocument pdfDocument = PDDocument.load(resolvedPdfPath.toFile())) { + Document newDocument = new Document(); + addIdentifiers(newDocument, pdf.getLink()); + addContentIfNotEmpty(pdfDocument, newDocument); + addMetaData(newDocument, resolvedPdfPath); + return newDocument; + } + } + + private void addMetaData(Document newDocument, Path resolvedPdfPath) { + try { + BasicFileAttributes attributes = Files.readAttributes(resolvedPdfPath, BasicFileAttributes.class); + addStringField(newDocument, MODIFIED, String.valueOf(attributes.lastModifiedTime().to(TimeUnit.SECONDS))); + } catch (IOException e) { + LOGGER.error("Could not read timestamp for {}", resolvedPdfPath, e); + } + } + + private void addStringField(Document newDocument, String field, String value) { + if (!isValidField(value)) { + return; + } + newDocument.add(new StringField(field, value, Field.Store.YES)); + } + + private boolean isValidField(String value) { + return !(StringUtil.isNullOrEmpty(value)); + } + + private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument) { + try { + PDFTextStripper pdfTextStripper = new PDFTextStripper(); + pdfTextStripper.setLineSeparator("\n"); + + String pdfContent = pdfTextStripper.getText(pdfDocument); + if (StringUtil.isNotBlank(pdfContent)) { + newDocument.add(new TextField(CONTENT, pdfContent, Field.Store.YES)); + } + for (PDPage page : pdfDocument.getPages()) { + for (PDAnnotation annotation : page.getAnnotations(annotation -> { + if (annotation.getContents() == null) { + return false; + } + return annotation.getSubtype().equals("Text") || annotation.getSubtype().equals("Highlight"); + })) { + newDocument.add(new TextField(ANNOTATIONS, annotation.getContents(), Field.Store.YES)); + } + } + } catch (IOException e) { + LOGGER.info("Could not read contents of PDF document \"{}\"", pdfDocument.toString(), e); + } + } + + private void addIdentifiers(Document newDocument, String path) { + newDocument.add(new StringField(PATH, path, Field.Store.YES)); + } +} 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 new file mode 100644 index 00000000000..78c0f1d4328 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java @@ -0,0 +1,126 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabase; +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 { + + Queue> taskQueue = new LinkedList<>(); + TaskExecutor taskExecutor; + + public IndexingTaskManager(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + showToUser(true); + // the task itself is a nop, but it's progress property will be updated by the child-tasks it creates that actually interact with the index + this.updateProgress(1, 1); + this.titleProperty().set(Localization.lang("Indexing pdf files")); + this.executeWith(taskExecutor); + } + + @Override + protected Void call() throws Exception { + // update index to make sure it is up to date + this.updateProgress(1, 1); + return null; + } + + private void enqueueTask(BackgroundTask task) { + task.onFinished(() -> { + this.progressProperty().unbind(); + this.updateProgress(1, 1); + taskQueue.poll(); // This is the task that just finished + if (!taskQueue.isEmpty()) { + BackgroundTask nextTask = taskQueue.poll(); + nextTask.executeWith(taskExecutor); + this.progressProperty().bind(nextTask.progressProperty()); + } + }); + taskQueue.add(task); + if (taskQueue.size() == 1) { + task.executeWith(taskExecutor); + this.progressProperty().bind(task.progressProperty()); + } + } + + public void createIndex(PdfIndexer indexer, BibDatabase database, BibDatabaseContext context) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.createIndex(database, context); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(databaseContext); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibEntry entry, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(entry, databaseContext); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(entry, linkedFiles, databaseContext); + return null; + } + }); + } + + public void removeFromIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.removeFromIndex(entry, linkedFiles); + return null; + } + }); + } + + public void removeFromIndex(PdfIndexer indexer, BibEntry entry) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.removeFromIndex(entry); + return null; + } + }); + } + + public void updateDatabaseName(String name) { + this.updateMessage(name); + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java b/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java new file mode 100644 index 00000000000..a28784cb73b --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java @@ -0,0 +1,232 @@ +package org.jabref.logic.pdf.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.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javafx.collections.ObservableList; + +import org.jabref.gui.LibraryTab; +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.pdf.search.EnglishStemAnalyzer; +import org.jabref.model.pdf.search.SearchFieldConstants; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.document.Document; +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.search.IndexSearcher; +import org.apache.lucene.search.TermQuery; +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; + +/** + * Indexes the text of PDF files and adds it into the lucene search index. + */ +public class PdfIndexer { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final Directory directoryToIndex; + private BibDatabaseContext databaseContext; + + private final FilePreferences filePreferences; + + public PdfIndexer(Directory indexDirectory, FilePreferences filePreferences) { + this.directoryToIndex = indexDirectory; + this.filePreferences = filePreferences; + } + + public static PdfIndexer of(BibDatabaseContext databaseContext, FilePreferences filePreferences) throws IOException { + return new PdfIndexer(new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); + } + + /** + * Adds all PDF files linked to an entry in the database to new Lucene search index. Any previous state of the + * Lucene search index will be deleted! + * + * @param database a bibtex database to link the pdf files to + */ + public void createIndex(BibDatabase database, BibDatabaseContext context) { + this.databaseContext = context; + final ObservableList entries = database.getEntries(); + + // Create new index by creating IndexWriter but not writing anything. + try { + IndexWriter indexWriter = new IndexWriter(directoryToIndex, new IndexWriterConfig(new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE)); + indexWriter.close(); + } catch (IOException e) { + LOGGER.warn("Could not create new Index!", e); + } + // Re-use existing facilities for writing the actual entries + entries.stream().filter(entry -> !entry.getFiles().isEmpty()).forEach(this::writeToIndex); + } + + 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); + } + } + + /** + * Adds a pdf file linked to one entry in the database to an existing (or new) Lucene search index + * + * @param entry a bibtex entry + * @param linkedFile the link to the pdf files + */ + public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext databaseContext) { + if (databaseContext != null) { + this.databaseContext = databaseContext; + } + if (!entry.getFiles().isEmpty()) { + writeToIndex(entry, linkedFile); + } + } + + /** + * Removes a pdf file linked to one entry in the database from the index + * @param entry the entry the file is linked to + * @param linkedFile the link to the file to be removed + */ + public void removeFromIndex(BibEntry entry, LinkedFile linkedFile) { + try (IndexWriter indexWriter = new IndexWriter( + directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)) + ) { + if (!entry.getFiles().isEmpty()) { + indexWriter.deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); + } + indexWriter.commit(); + } catch (IOException e) { + LOGGER.warn("Could not initialize the IndexWriter!", e); + } + } + + /** + * 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) { + removeFromIndex(entry, linkedFile); + } + } + + /** + * Deletes all entries from the Lucene search index. + */ + public void flushIndex() { + IndexWriterConfig config = new IndexWriterConfig(); + config.setOpenMode(IndexWriterConfig.OpenMode.CREATE); + try (IndexWriter deleter = new IndexWriter(directoryToIndex, config)) { + // Do nothing. Index is deleted. + } catch (IOException e) { + LOGGER.warn("The IndexWriter could not be initialized", e); + } + } + + /** + * 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); + } + } + + /** + * Writes the file to the index if the file is not yet in the index or the file on the fs is newer than the one in + * the index. + * @param entry the entry associated with the file + * @param linkedFile the file to write to the index + */ + private void writeToIndex(BibEntry entry, LinkedFile linkedFile) { + Optional resolvedPath = linkedFile.findIn(databaseContext, filePreferences); + if (resolvedPath.isEmpty()) { + LOGGER.warn("Could not find {}", linkedFile.getLink()); + return; + } + try { + // Check if a document with this path is already in the index + try { + IndexReader reader = DirectoryReader.open(directoryToIndex); + IndexSearcher searcher = new IndexSearcher(reader); + TermQuery query = new TermQuery(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); + TopDocs topDocs = searcher.search(query, 1); + // If a document was found, check if is less current than the one in the FS + if (topDocs.scoreDocs.length > 0) { + Document doc = reader.document(topDocs.scoreDocs[0].doc); + long indexModificationTime = Long.parseLong(doc.getField(SearchFieldConstants.MODIFIED).stringValue()); + + BasicFileAttributes attributes = Files.readAttributes(resolvedPath.get(), BasicFileAttributes.class); + + if (indexModificationTime >= attributes.lastModifiedTime().to(TimeUnit.SECONDS)) { + return; + } + } + reader.close(); + } catch (IndexNotFoundException e) { + // if there is no index yet, don't need to check anything! + } + // If no document was found, add the new one + Optional document = new DocumentReader(entry, filePreferences).readLinkedPdf(this.databaseContext, linkedFile); + if (document.isPresent()) { + IndexWriter indexWriter = new IndexWriter(directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)); + indexWriter.addDocument(document.get()); + indexWriter.commit(); + indexWriter.close(); + } + } catch (IOException e) { + LOGGER.warn("Could not add the document to the index!", e); + } + } +} 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 new file mode 100644 index 00000000000..59d320e393f --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java @@ -0,0 +1,76 @@ +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); + } + + try { + List resultDocs = new LinkedList<>(); + + 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: '" + searchString + "'! \n" + e.getMessage()); + return new PdfSearchResults(); + } + } +} diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index 5138edcf178..883830c3eab 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -1,6 +1,7 @@ package org.jabref.logic.search; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -18,6 +19,7 @@ import org.jabref.model.search.rules.SentenceAnalyzer; public class SearchQuery implements SearchMatcher { + /** * The mode of escaping special characters in regular expressions */ @@ -56,15 +58,13 @@ String format(String regex) { } private final String query; - private final boolean caseSensitive; - private final boolean regularExpression; + private EnumSet searchFlags; private final SearchRule rule; - public SearchQuery(String query, boolean caseSensitive, boolean regularExpression) { + public SearchQuery(String query, EnumSet searchFlags) { this.query = Objects.requireNonNull(query); - this.caseSensitive = caseSensitive; - this.regularExpression = regularExpression; - this.rule = SearchRules.getSearchRuleByQuery(query, caseSensitive, regularExpression); + this.searchFlags = searchFlags; + this.rule = SearchRules.getSearchRuleByQuery(query, searchFlags); } @Override @@ -86,7 +86,7 @@ public boolean isContainsBasedSearch() { } private String getCaseSensitiveDescription() { - if (isCaseSensitive()) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return "case sensitive"; } else { return "case insensitive"; @@ -94,7 +94,7 @@ private String getCaseSensitiveDescription() { } private String getRegularExpressionDescription() { - if (isRegularExpression()) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return "regular expression"; } else { return "plain text"; @@ -109,7 +109,7 @@ public String localize() { } private String getLocalizedCaseSensitiveDescription() { - if (isCaseSensitive()) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return Localization.lang("case sensitive"); } else { return Localization.lang("case insensitive"); @@ -117,7 +117,7 @@ private String getLocalizedCaseSensitiveDescription() { } private String getLocalizedRegularExpressionDescription() { - if (isRegularExpression()) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return Localization.lang("regular expression"); } else { return Localization.lang("plain text"); @@ -137,19 +137,15 @@ public String getQuery() { return query; } - public boolean isCaseSensitive() { - return caseSensitive; - } - - public boolean isRegularExpression() { - return regularExpression; + public EnumSet getSearchFlags() { + return searchFlags; } /** * Returns a list of words this query searches for. The returned strings can be a regular expression. */ public List getSearchWords() { - if (isRegularExpression()) { + 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. @@ -182,13 +178,13 @@ private Optional joinWordsToPattern(EscapeMode escapeMode) { // compile the words to a regular expression in the form (w1)|(w2)|(w3) Stream joiner = words.stream(); - if (!regularExpression) { + 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 (caseSensitive) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return Optional.of(Pattern.compile(searchPattern)); } else { return Optional.of(Pattern.compile(searchPattern, Pattern.CASE_INSENSITIVE)); diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 2b0e713614f..1037b1bc0c6 100644 --- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -9,13 +9,19 @@ import java.util.stream.Collectors; import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.LibraryTab; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.shared.DatabaseSynchronizer; import org.jabref.logic.util.CoarseChangeFilter; import org.jabref.model.entry.BibEntry; import org.jabref.model.metadata.MetaData; +import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.preferences.FilePreferences; +import net.harawata.appdirs.AppDirsFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Represents everything related to a BIB file.

The entries are stored in BibDatabase, the other data in MetaData * and the options relevant for this file in Defaults. @@ -23,6 +29,10 @@ @AllowedToUseLogic("because it needs access to shared database features") public class BibDatabaseContext { + public static final String SEARCH_INDEX_BASE_PATH = "JabRef"; + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + private final BibDatabase database; private MetaData metaData; @@ -191,14 +201,6 @@ public void convertToSharedDatabase(DatabaseSynchronizer dmbsSynchronizer) { this.location = DatabaseLocation.SHARED; } - @Override - public String toString() { - return "BibDatabaseContext{" + - "path=" + path + - ", location=" + location + - '}'; - } - public void convertToLocalDatabase() { if (Objects.nonNull(dbmsListener) && (location == DatabaseLocation.SHARED)) { dbmsListener.unregisterListener(dbmsSynchronizer); @@ -211,4 +213,28 @@ public void convertToLocalDatabase() { public List getEntries() { return database.getEntries(); } + + public static Path getFulltextIndexBasePath() { + return Path.of(AppDirsFactory.getInstance().getUserDataDir(SEARCH_INDEX_BASE_PATH, SearchFieldConstants.VERSION, "org.jabref")); + } + + public Path getFulltextIndexPath() { + Path appData = getFulltextIndexBasePath(); + LOGGER.info("Index path for {} is {}", getDatabasePath().get(), appData.toString()); + if (getDatabasePath().isPresent()) { + return appData.resolve(String.valueOf(this.getDatabasePath().get().hashCode())); + } + return appData.resolve("unsaved"); + } + + @Override + public String toString() { + return "BibDatabaseContext{" + + "metaData=" + metaData + + ", mode=" + getMode() + + ", databasePath=" + getDatabasePath() + + ", biblatexMode=" + isBiblatexMode() + + ", fulltextIndexPath=" + getFulltextIndexPath() + + '}'; + } } diff --git a/src/main/java/org/jabref/model/groups/SearchGroup.java b/src/main/java/org/jabref/model/groups/SearchGroup.java index 2346c23c10a..5e261c438b9 100644 --- a/src/main/java/org/jabref/model/groups/SearchGroup.java +++ b/src/main/java/org/jabref/model/groups/SearchGroup.java @@ -1,9 +1,11 @@ package org.jabref.model.groups; +import java.util.EnumSet; import java.util.Objects; import org.jabref.model.entry.BibEntry; import org.jabref.model.search.GroupSearchQuery; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,10 +19,9 @@ public class SearchGroup extends AbstractGroup { private static final Logger LOGGER = LoggerFactory.getLogger(SearchGroup.class); private final GroupSearchQuery query; - public SearchGroup(String name, GroupHierarchyType context, String searchExpression, boolean caseSensitive, - boolean isRegEx) { + public SearchGroup(String name, GroupHierarchyType context, String searchExpression, EnumSet searchFlags) { super(name, context); - this.query = new GroupSearchQuery(searchExpression, caseSensitive, isRegEx); + this.query = new GroupSearchQuery(searchExpression, searchFlags); } public String getSearchExpression() { @@ -38,8 +39,7 @@ public boolean equals(Object o) { SearchGroup other = (SearchGroup) o; return getName().equals(other.getName()) && getSearchExpression().equals(other.getSearchExpression()) - && (isCaseSensitive() == other.isCaseSensitive()) - && (isRegularExpression() == other.isRegularExpression()) + && (getSearchFlags().equals(other.getSearchFlags())) && (getHierarchicalContext() == other.getHierarchicalContext()); } @@ -48,11 +48,14 @@ public boolean contains(BibEntry entry) { return query.isMatch(entry); } + public EnumSet getSearchFlags() { + return query.getSearchFlags(); + } + @Override public AbstractGroup deepCopy() { try { - return new SearchGroup(getName(), getHierarchicalContext(), getSearchExpression(), isCaseSensitive(), - isRegularExpression()); + return new SearchGroup(getName(), getHierarchicalContext(), getSearchExpression(), getSearchFlags()); } catch (Throwable t) { // this should never happen, because the constructor obviously // succeeded in creating _this_ instance! @@ -62,14 +65,6 @@ public AbstractGroup deepCopy() { } } - public boolean isCaseSensitive() { - return query.isCaseSensitive(); - } - - public boolean isRegularExpression() { - return query.isRegularExpression(); - } - @Override public boolean isDynamic() { return true; @@ -77,6 +72,6 @@ public boolean isDynamic() { @Override public int hashCode() { - return Objects.hash(getName(), getHierarchicalContext(), getSearchExpression(), isCaseSensitive(), isRegularExpression()); + return Objects.hash(getName(), getHierarchicalContext(), getSearchExpression(), getSearchFlags()); } } diff --git a/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java b/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java new file mode 100644 index 00000000000..1dcccbb6583 --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java @@ -0,0 +1,25 @@ +package org.jabref.model.pdf.search; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.StopFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.core.DecimalDigitFilter; +import org.apache.lucene.analysis.en.EnglishAnalyzer; +import org.apache.lucene.analysis.en.PorterStemFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +public class EnglishStemAnalyzer extends Analyzer { + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer source = new StandardTokenizer(); + TokenStream filter = new LowerCaseFilter(source); + filter = new StopFilter(filter, EnglishAnalyzer.ENGLISH_STOP_WORDS_SET); + filter = new DecimalDigitFilter(filter); + filter = new PorterStemFilter(filter); + return new TokenStreamComponents(source, filter); + } +} + diff --git a/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java b/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java new file mode 100644 index 00000000000..2c6ad8e9d89 --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java @@ -0,0 +1,32 @@ +package org.jabref.model.pdf.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PdfSearchResults { + + private final List searchResults; + + public PdfSearchResults(List search) { + this.searchResults = Collections.unmodifiableList(search); + } + + public PdfSearchResults() { + this.searchResults = Collections.emptyList(); + } + + public List getSortedByScore() { + List sortedList = new ArrayList<>(searchResults); + sortedList.sort((searchResult, t1) -> Float.compare(searchResult.getLuceneScore(), t1.getLuceneScore())); + return Collections.unmodifiableList(sortedList); + } + + public List getSearchResults() { + return this.searchResults; + } + + public int numSearchResults() { + return this.searchResults.size(); + } +} diff --git a/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java new file mode 100644 index 00000000000..ca72a1fac1b --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java @@ -0,0 +1,13 @@ +package org.jabref.model.pdf.search; + +public class SearchFieldConstants { + + public static final String PATH = "path"; + public static final String CONTENT = "content"; + public static final String ANNOTATIONS = "annotations"; + public static final String MODIFIED = "modified"; + + public static final String[] PDF_FIELDS = new String[]{PATH, CONTENT, MODIFIED, ANNOTATIONS}; + + public static final String VERSION = "0.3a"; +} diff --git a/src/main/java/org/jabref/model/pdf/search/SearchResult.java b/src/main/java/org/jabref/model/pdf/search/SearchResult.java new file mode 100644 index 00000000000..ea3dd319e6c --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/SearchResult.java @@ -0,0 +1,83 @@ +package org.jabref.model.pdf.search; + +import java.io.IOException; + +import org.jabref.model.entry.BibEntry; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.highlight.Highlighter; +import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; +import org.apache.lucene.search.highlight.QueryScorer; +import org.apache.lucene.search.highlight.SimpleHTMLFormatter; +import org.apache.lucene.search.highlight.TextFragment; + +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.PATH; + +public final class SearchResult { + + private final String path; + private final String content; + private final long modified; + + private final float luceneScore; + private String html; + + public SearchResult(IndexSearcher searcher, Query query, ScoreDoc scoreDoc) throws IOException { + this.path = getFieldContents(searcher, scoreDoc, PATH); + this.content = getFieldContents(searcher, scoreDoc, CONTENT); + this.modified = Long.parseLong(getFieldContents(searcher, scoreDoc, MODIFIED)); + this.luceneScore = scoreDoc.score; + + TokenStream stream = new EnglishStemAnalyzer().tokenStream(CONTENT, content); + + Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter(), new QueryScorer(query)); + try { + + TextFragment[] frags = highlighter.getBestTextFragments(stream, content, true, 10); + this.html = ""; + for (TextFragment frag : frags) { + html += "

" + frag.toString() + "

"; + } + } catch (InvalidTokenOffsetsException e) { + this.html = ""; + } + } + + private String getFieldContents(IndexSearcher searcher, ScoreDoc scoreDoc, String field) throws IOException { + IndexableField indexableField = searcher.doc(scoreDoc.doc).getField(field); + if (indexableField == null) { + return ""; + } + return indexableField.stringValue(); + } + + public boolean isResultFor(BibEntry entry) { + return entry.getFiles().stream().anyMatch(linkedFile -> path.equals(linkedFile.getLink())); + } + + public String getPath() { + return path; + } + + public String getContent() { + return content; + } + + public long getModified() { + return modified; + } + + public float getLuceneScore() { + return luceneScore; + } + + public String getHtml() { + return html; + } +} diff --git a/src/main/java/org/jabref/model/search/GroupSearchQuery.java b/src/main/java/org/jabref/model/search/GroupSearchQuery.java index 3eea63f99d9..90056d9b0f7 100644 --- a/src/main/java/org/jabref/model/search/GroupSearchQuery.java +++ b/src/main/java/org/jabref/model/search/GroupSearchQuery.java @@ -1,22 +1,22 @@ 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.model.search.rules.SearchRules.SearchFlags; public class GroupSearchQuery implements SearchMatcher { private final String query; - private final boolean caseSensitive; - private final boolean regularExpression; + private final EnumSet searchFlags; private final SearchRule rule; - public GroupSearchQuery(String query, boolean caseSensitive, boolean regularExpression) { + public GroupSearchQuery(String query, EnumSet searchFlags) { this.query = Objects.requireNonNull(query); - this.caseSensitive = caseSensitive; - this.regularExpression = regularExpression; + this.searchFlags = searchFlags; this.rule = Objects.requireNonNull(getSearchRule()); } @@ -32,11 +32,11 @@ public boolean isMatch(BibEntry entry) { } private SearchRule getSearchRule() { - return SearchRules.getSearchRuleByQuery(query, caseSensitive, regularExpression); + return SearchRules.getSearchRuleByQuery(query, searchFlags); } private String getCaseSensitiveDescription() { - if (caseSensitive) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return "case sensitive"; } else { return "case insensitive"; @@ -44,7 +44,7 @@ private String getCaseSensitiveDescription() { } private String getRegularExpressionDescription() { - if (regularExpression) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return "regular expression"; } else { return "plain text"; @@ -59,11 +59,7 @@ public String getSearchExpression() { return query; } - public boolean isCaseSensitive() { - return caseSensitive; - } - - public boolean isRegularExpression() { - return regularExpression; + public EnumSet getSearchFlags() { + return searchFlags; } } diff --git a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java index f77c57324d2..cf7c7568368 100644 --- a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java @@ -1,25 +1,48 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Vector; +import java.util.stream.Collectors; +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.Globals; +import org.jabref.gui.LibraryTab; +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.field.Field; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Search rule for contain-based search. */ +@AllowedToUseLogic("Because access to the lucene index is needed") public class ContainBasedSearchRule implements SearchRule { - private final boolean caseSensitive; + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - public ContainBasedSearchRule(boolean caseSensitive) { - this.caseSensitive = caseSensitive; - } + private final EnumSet searchFlags; + + private String lastQuery; + private List lastSearchResults; + + private final BibDatabaseContext databaseContext; - public boolean isCaseSensitive() { - return caseSensitive; + public ContainBasedSearchRule(EnumSet searchFlags) { + this.searchFlags = searchFlags; + this.lastQuery = ""; + lastSearchResults = new Vector<>(); + + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } @Override @@ -31,7 +54,7 @@ public boolean validateSearchStrings(String query) { public boolean applyRule(String query, BibEntry bibEntry) { String searchString = query; - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { searchString = searchString.toLowerCase(Locale.ROOT); } @@ -39,7 +62,7 @@ public boolean applyRule(String query, BibEntry bibEntry) { for (Field fieldKey : bibEntry.getFields()) { String formattedFieldContent = bibEntry.getLatexFreeField(fieldKey).get(); - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { formattedFieldContent = formattedFieldContent.toLowerCase(Locale.ROOT); } @@ -56,6 +79,32 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } - return false; // Didn't match all words. + return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. + } + + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return new PdfSearchResults(List.of()); + } + + if (!query.equals(this.lastQuery)) { + this.lastQuery = query; + lastSearchResults = List.of(); + 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())); + } + + public EnumSet getSearchFlags() { + return searchFlags; } } diff --git a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java index 98e12978963..5b0b5c40ea3 100644 --- a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java @@ -1,5 +1,8 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -8,10 +11,17 @@ 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.search.SearchBaseVisitor; import org.jabref.search.SearchLexer; import org.jabref.search.SearchParser; @@ -32,15 +42,18 @@ *

* 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 boolean caseSensitiveSearch; - private final boolean regExpSearch; + private final EnumSet searchFlags; private ParseTree tree; private String query; + private List searchResults; + + private final BibDatabaseContext databaseContext; public static class ThrowingErrorListener extends BaseErrorListener { @@ -54,21 +67,13 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, } } - public GrammarBasedSearchRule(boolean caseSensitiveSearch, boolean regExpSearch) throws RecognitionException { - this.caseSensitiveSearch = caseSensitiveSearch; - this.regExpSearch = regExpSearch; - } - - public static boolean isValid(boolean caseSensitive, boolean regExp, String query) { - return new GrammarBasedSearchRule(caseSensitive, regExp).validateSearchStrings(query); - } - - public boolean isCaseSensitiveSearch() { - return this.caseSensitiveSearch; + public GrammarBasedSearchRule(EnumSet searchFlags) throws RecognitionException { + this.searchFlags = searchFlags; + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } - public boolean isRegExpSearch() { - return this.regExpSearch; + public static boolean isValid(EnumSet searchFlags, String query) { + return new GrammarBasedSearchRule(searchFlags).validateSearchStrings(query); } public ParseTree getTree() { @@ -93,18 +98,34 @@ private void init(String query) throws ParseCancellationException { 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(caseSensitiveSearch, regExpSearch, bibEntry).visit(tree); + return new BibtexSearchVisitor(searchFlags, bibEntry).visit(tree); } catch (Exception e) { LOGGER.debug("Search failed", e); - return false; + 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 { @@ -116,6 +137,10 @@ public boolean validateSearchStrings(String query) { } } + public EnumSet getSearchFlags() { + return searchFlags; + } + public enum ComparisonOperator { EXACT, CONTAINS, DOES_NOT_CONTAIN; @@ -136,12 +161,12 @@ public static class Comparator { private final Pattern fieldPattern; private final Pattern valuePattern; - public Comparator(String field, String value, ComparisonOperator operator, boolean caseSensitive, boolean regex) { + public Comparator(String field, String value, ComparisonOperator operator, EnumSet searchFlags) { this.operator = operator; - int option = caseSensitive ? 0 : Pattern.CASE_INSENSITIVE; - this.fieldPattern = Pattern.compile(regex ? field : "\\Q" + field + "\\E", option); - this.valuePattern = Pattern.compile(regex ? value : "\\Q" + value + "\\E", option); + int option = searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE; + this.fieldPattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? field : "\\Q" + field + "\\E", option); + this.valuePattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? value : "\\Q" + value + "\\E", option); } public boolean compare(BibEntry entry) { @@ -200,19 +225,17 @@ public boolean matchFieldValue(String content) { */ static class BibtexSearchVisitor extends SearchBaseVisitor { - private final boolean caseSensitive; - private final boolean regex; + private final EnumSet searchFlags; private final BibEntry entry; - public BibtexSearchVisitor(boolean caseSensitive, boolean regex, BibEntry bibEntry) { - this.caseSensitive = caseSensitive; - this.regex = regex; + 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, caseSensitive, regex).compare(entry); + return new Comparator(field, value, operator, searchFlags).compare(entry); } @Override @@ -232,7 +255,7 @@ public Boolean visitComparison(SearchParser.ComparisonContext context) { if (fieldDescriptor.isPresent()) { return comparison(fieldDescriptor.get().getText(), ComparisonOperator.build(context.operator.getText()), right); } else { - return SearchRules.getSearchRule(caseSensitive, regex).applyRule(right, entry); + return SearchRules.getSearchRule(searchFlags).applyRule(right, entry); } } diff --git a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java index 9d43710c655..0cc4bcb1f0a 100644 --- a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java @@ -1,38 +1,62 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +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.field.Field; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +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 implements SearchRule { - private final boolean caseSensitive; + private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); + + private final EnumSet searchFlags; + + private String lastQuery; + private List lastSearchResults; + + private final BibDatabaseContext databaseContext; - public RegexBasedSearchRule(boolean caseSensitive) { - this.caseSensitive = caseSensitive; + public RegexBasedSearchRule(EnumSet searchFlags) { + this.searchFlags = searchFlags; + + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } - public boolean isCaseSensitive() { - return caseSensitive; + public EnumSet getSearchFlags() { + return searchFlags; } @Override public boolean validateSearchStrings(String query) { String searchString = query; - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { searchString = searchString.toLowerCase(Locale.ROOT); } try { - Pattern.compile(searchString, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); + Pattern.compile(searchString, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { return false; } @@ -44,7 +68,7 @@ public boolean applyRule(String query, BibEntry bibEntry) { Pattern pattern; try { - pattern = Pattern.compile(query, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); + pattern = Pattern.compile(query, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { return false; } @@ -59,6 +83,27 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } } - return false; + return getFulltextResults(query, bibEntry).numSearchResults() > 0; + } + + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return new PdfSearchResults(List.of()); + } + + if (!query.equals(this.lastQuery)) { + this.lastQuery = query; + lastSearchResults = List.of(); + 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/SearchRule.java b/src/main/java/org/jabref/model/search/rules/SearchRule.java index ffc4dced699..1be2b05b342 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRule.java @@ -1,10 +1,13 @@ 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 dcde2b66227..9b6ffb51b77 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -1,5 +1,6 @@ package org.jabref.model.search.rules; +import java.util.EnumSet; import java.util.regex.Pattern; public class SearchRules { @@ -12,18 +13,18 @@ private SearchRules() { /** * Returns the appropriate search rule that fits best to the given parameter. */ - public static SearchRule getSearchRuleByQuery(String query, boolean caseSensitive, boolean regex) { + public static SearchRule getSearchRuleByQuery(String query, EnumSet searchFlags) { if (isSimpleQuery(query)) { - return new ContainBasedSearchRule(caseSensitive); + return new ContainBasedSearchRule(searchFlags); } // this searches specified fields if specified, // and all fields otherwise - SearchRule searchExpression = new GrammarBasedSearchRule(caseSensitive, regex); + SearchRule searchExpression = new GrammarBasedSearchRule(searchFlags); if (searchExpression.validateSearchStrings(query)) { return searchExpression; } else { - return getSearchRule(caseSensitive, regex); + return getSearchRule(searchFlags); } } @@ -31,11 +32,15 @@ private static boolean isSimpleQuery(String query) { return SIMPLE_EXPRESSION.matcher(query).matches(); } - static SearchRule getSearchRule(boolean caseSensitive, boolean regex) { - if (regex) { - return new RegexBasedSearchRule(caseSensitive); + static SearchRule getSearchRule(EnumSet searchFlags) { + if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) { + return new RegexBasedSearchRule(searchFlags); } else { - return new ContainBasedSearchRule(caseSensitive); + return new ContainBasedSearchRule(searchFlags); } } + + public enum SearchFlags { + CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT; + } } diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index ba5df0fa216..8a459c260ea 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -236,6 +236,7 @@ public class JabRefPreferences implements PreferencesService { 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 GENERATE_KEY_ON_IMPORT = "generateKeyOnImport"; @@ -438,6 +439,7 @@ private JabRefPreferences() { 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.TRUE); defaults.put(GENERATE_KEY_ON_IMPORT, Boolean.TRUE); @@ -2532,7 +2534,8 @@ public SearchPreferences getSearchPreferences() { return new SearchPreferences( searchDisplayMode, getBoolean(SEARCH_CASE_SENSITIVE), - getBoolean(SEARCH_REG_EXP)); + getBoolean(SEARCH_REG_EXP), + getBoolean(SEARCH_FULLTEXT)); } @Override @@ -2540,6 +2543,7 @@ public void storeSearchPreferences(SearchPreferences preferences) { put(SEARCH_DISPLAY_MODE, Objects.requireNonNull(preferences.getSearchDisplayMode()).toString()); putBoolean(SEARCH_CASE_SENSITIVE, preferences.isCaseSensitive()); putBoolean(SEARCH_REG_EXP, preferences.isRegularExpression()); + putBoolean(SEARCH_FULLTEXT, preferences.isFulltext()); } //************************************************************************************************************* diff --git a/src/main/java/org/jabref/preferences/SearchPreferences.java b/src/main/java/org/jabref/preferences/SearchPreferences.java index e85594dd7b2..2674c0e897f 100644 --- a/src/main/java/org/jabref/preferences/SearchPreferences.java +++ b/src/main/java/org/jabref/preferences/SearchPreferences.java @@ -1,17 +1,33 @@ package org.jabref.preferences; +import java.util.EnumSet; + import org.jabref.gui.search.SearchDisplayMode; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; public class SearchPreferences { private final SearchDisplayMode searchDisplayMode; - private final boolean isCaseSensitive; - private final boolean isRegularExpression; + private final EnumSet searchFlags; - public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression) { + public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext) { this.searchDisplayMode = searchDisplayMode; - this.isCaseSensitive = isCaseSensitive; - this.isRegularExpression = isRegularExpression; + searchFlags = EnumSet.noneOf(SearchFlags.class); + if (isCaseSensitive) { + searchFlags.add(SearchFlags.CASE_SENSITIVE); + } + if (isRegularExpression) { + searchFlags.add(SearchFlags.REGULAR_EXPRESSION); + } + if (isFulltext) { + searchFlags.add(SearchFlags.FULLTEXT); + } + } + + public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags) { + this.searchDisplayMode = searchDisplayMode; + this.searchFlags = searchFlags; } public SearchDisplayMode getSearchDisplayMode() { @@ -19,22 +35,44 @@ public SearchDisplayMode getSearchDisplayMode() { } public boolean isCaseSensitive() { - return isCaseSensitive; + return searchFlags.contains(SearchFlags.CASE_SENSITIVE); } public boolean isRegularExpression() { - return isRegularExpression; + return searchFlags.contains(SearchFlags.REGULAR_EXPRESSION); + } + + public boolean isFulltext() { + return searchFlags.contains(SearchFlags.FULLTEXT); + } + + public EnumSet getSearchFlags() { + EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); + if (isCaseSensitive()) { + searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); + } + if (isRegularExpression()) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } + if (isFulltext()) { + searchFlags.add(SearchRules.SearchFlags.FULLTEXT); + } + return searchFlags; } public SearchPreferences withSearchDisplayMode(SearchDisplayMode newSearchDisplayMode) { - return new SearchPreferences(newSearchDisplayMode, isCaseSensitive, isRegularExpression); + return new SearchPreferences(newSearchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext()); } public SearchPreferences withCaseSensitive(boolean newCaseSensitive) { - return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression); + return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression(), isFulltext()); } public SearchPreferences withRegularExpression(boolean newRegularExpression) { - return new SearchPreferences(searchDisplayMode, isCaseSensitive, newRegularExpression); + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), newRegularExpression, isFulltext()); + } + + public SearchPreferences withFulltext(boolean newFulltext) { + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), newFulltext); } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index c6a9db1ab7c..8abf5a998e1 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -363,6 +363,8 @@ Formatter\ name=Formatter name found\ in\ AUX\ file=found in AUX file +Fulltext\ search=Fulltext search + Fulltext\ for=Fulltext for Further\ information\ about\ Mr.\ DLib\ for\ JabRef\ users.=Further information about Mr. DLib for JabRef users. @@ -440,6 +442,8 @@ Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Independent group: When selected, view only this group's entries I\ Agree=I Agree +Indexing\ pdf\ files=Indexing pdf files + Invalid\ citation\ key=Invalid citation key Invalid\ URL=Invalid URL @@ -2354,3 +2358,8 @@ Query=Query Question=Question Select\ directory=Select directory +Rebuild\ fulltext\ search\ index=Rebuild fulltext search index +Rebuild\ fulltext\ search\ index\ for\ current\ library?=Rebuild fulltext search index for current library? +Rebuilding\ fulltext\ search\ index...=Rebuilding fulltext search index... +Failed\ to\ access\ fulltext\ search\ index=Failed to access fulltext search index +Found\ match\ in\ %0=Found match in %0 diff --git a/src/main/resources/luceneIndex/.gitignore b/src/main/resources/luceneIndex/.gitignore new file mode 100644 index 00000000000..72e8ffc0db8 --- /dev/null +++ b/src/main/resources/luceneIndex/.gitignore @@ -0,0 +1 @@ +* diff --git a/src/main/resources/luceneIndex/.gitkeep b/src/main/resources/luceneIndex/.gitkeep new file mode 100644 index 00000000000..ef056e1e450 --- /dev/null +++ b/src/main/resources/luceneIndex/.gitkeep @@ -0,0 +1,2 @@ +# For ensuring this directory is not deleted. +# This directory is used for the Lucene indices. diff --git a/src/test/java/org/jabref/architecture/MainArchitectureTests.java b/src/test/java/org/jabref/architecture/MainArchitectureTests.java index a6459ed2fe3..40e3d945f2b 100644 --- a/src/test/java/org/jabref/architecture/MainArchitectureTests.java +++ b/src/test/java/org/jabref/architecture/MainArchitectureTests.java @@ -117,7 +117,12 @@ public static void doNotUseLogicInModel(JavaClasses classes) { @ArchTest public static void restrictUsagesInModel(JavaClasses classes) { - noClasses().that().resideInAPackage(PACKAGE_ORG_JABREF_MODEL) + // Until we switch to Lucene, we need to access Globals.stateManager().getActiveDatabase() from the search classes, + // because the PDFSearch needs to access the index of the corresponding database + noClasses().that().areNotAssignableFrom("org.jabref.model.search.rules.ContainBasedSearchRule") + .and().areNotAssignableFrom("org.jabref.model.search.rules.RegexBasedSearchRule") + .and().areNotAssignableFrom("org.jabref.model.search.rules.GrammarBasedSearchRule") + .and().resideInAPackage(PACKAGE_ORG_JABREF_MODEL) .should().dependOnClassesThat().resideInAPackage(PACKAGE_JAVAX_SWING) .orShould().dependOnClassesThat().haveFullyQualifiedName(CLASS_ORG_JABREF_GLOBALS) .check(classes); diff --git a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java b/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java index b67da25d1ca..bc3406ec43c 100644 --- a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java +++ b/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java @@ -1,5 +1,6 @@ package org.jabref.gui.search; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -8,6 +9,8 @@ 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; @@ -32,7 +35,7 @@ void testSimpleTerm() { 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(false, false, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -46,7 +49,7 @@ void testNoAst() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(false, false, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -60,7 +63,7 @@ void testNoAstRegex() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(false, true, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -74,7 +77,7 @@ void testNoAstRegexCaseSensitive() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(true, true, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -88,7 +91,7 @@ void testNoAstCaseSensitive() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(true, false, query).getDescription(); + 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 index 36ae0dbea71..cad5aa79e89 100644 --- a/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java +++ b/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java @@ -1,6 +1,7 @@ package org.jabref.gui.search; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -10,6 +11,8 @@ 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; @@ -29,10 +32,10 @@ void onStart(Stage stage) { stage.show(); } - private TextFlow createDescription(String query, boolean caseSensitive, boolean regExp) { - GrammarBasedSearchRule grammarBasedSearchRule = new GrammarBasedSearchRule(caseSensitive, regExp); + private TextFlow createDescription(String query, EnumSet searchFlags) { + GrammarBasedSearchRule grammarBasedSearchRule = new GrammarBasedSearchRule(searchFlags); assertTrue(grammarBasedSearchRule.validateSearchStrings(query)); - GrammarBasedSearchRuleDescriber describer = new GrammarBasedSearchRuleDescriber(caseSensitive, regExp, grammarBasedSearchRule.getTree()); + GrammarBasedSearchRuleDescriber describer = new GrammarBasedSearchRuleDescriber(searchFlags, grammarBasedSearchRule.getTree()); return describer.getDescription(); } @@ -42,7 +45,7 @@ void testSimpleQueryCaseSensitiveRegex() { 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, true, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -53,7 +56,7 @@ void testSimpleQueryCaseSensitive() { 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, true, false); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -64,7 +67,7 @@ void testSimpleQuery() { 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, false, false); + TextFlow description = createDescription(query, EnumSet.noneOf(SearchFlags.class)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -75,7 +78,7 @@ void testSimpleQueryRegex() { 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, false, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -87,7 +90,7 @@ void testComplexQueryCaseSensitiveRegex() { 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, true, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -99,7 +102,7 @@ void testComplexQueryRegex() { 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, false, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -110,7 +113,7 @@ void testComplexQueryCaseSensitive() { 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, true, false); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -121,7 +124,7 @@ void testComplexQuery() { 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, false, false); + 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 4c1bc3147fb..1753495506a 100644 --- a/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java +++ b/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java @@ -3,6 +3,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import javafx.scene.paint.Color; @@ -24,6 +25,7 @@ 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.util.DummyFileUpdateMonitor; import org.junit.jupiter.api.BeforeEach; @@ -89,14 +91,14 @@ void serializeSingleRegexKeywordGroup() { @Test void serializeSingleSearchGroup() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", true, true); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, 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\"", true, false); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INCLUDING, "author=\"harrer\"", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); 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 new file mode 100644 index 00000000000..d8da1977f29 --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java @@ -0,0 +1,50 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +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.apache.lucene.document.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DocumentReaderTest { + + private BibDatabaseContext databaseContext; + private FilePreferences filePreferences; + + @BeforeEach + public void setup() { + this.databaseContext = mock(BibDatabaseContext.class); + when(databaseContext.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + this.filePreferences = mock(FilePreferences.class); + when(filePreferences.getUser()).thenReturn("test"); + when(filePreferences.getFileDirectory()).thenReturn(Optional.empty()); + when(filePreferences.shouldStoreFilesRelativeToBib()).thenReturn(true); + } + + @Test + public void unknownFileTestShouldReturnEmptyList() throws IOException { + // given + BibEntry entry = new BibEntry(); + entry.setFiles(Collections.singletonList(new LinkedFile("Wrong path", "NOT_PRESENT.pdf", "Type"))); + + // when + final List emptyDocumentList = new DocumentReader(entry, filePreferences).readLinkedPdfs(databaseContext); + + // then + assertEquals(Collections.emptyList(), emptyDocumentList); + } +} diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java b/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java new file mode 100644 index 00000000000..9dc45525ac8 --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java @@ -0,0 +1,147 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; + +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.preferences.FilePreferences; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.store.NIOFSDirectory; +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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PdfIndexerTest { + + private PdfIndexer indexer; + private BibDatabase database; + private BibDatabaseContext context = mock(BibDatabaseContext.class); + + @BeforeEach + public void setUp(@TempDir Path indexDir) throws IOException { + FilePreferences filePreferences = mock(FilePreferences.class); + this.database = new BibDatabase(); + + this.context = mock(BibDatabaseContext.class); + when(context.getDatabasePath()).thenReturn(Optional.of(Path.of("src/test/resources/pdfs/"))); + when(context.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + when(context.getFulltextIndexPath()).thenReturn(indexDir); + when(context.getDatabase()).thenReturn(database); + this.indexer = PdfIndexer.of(context, filePreferences); + } + + @Test + public void exampleThesisIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void exampleThesisIndexWithKey() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setCitationKey("Example2017"); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void metaDataIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.Article); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "metaData.pdf", StandardFileType.PDF.getName()))); + + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void testFlushIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setCitationKey("Example2017"); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + indexer.createIndex(database, context); + // index actually exists + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + + // when + indexer.flushIndex(); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(0, reader.numDocs()); + } + } + + @Test + public void exampleThesisIndexAppendMetaData() throws IOException { + // given + BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis); + exampleThesis.setCitationKey("ExampleThesis2017"); + exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(exampleThesis); + indexer.createIndex(database, context); + + // index with first entry + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + + BibEntry metadata = new BibEntry(StandardEntryType.Article); + metadata.setCitationKey("MetaData2017"); + metadata.setFiles(Collections.singletonList(new LinkedFile("Metadata file", "metaData.pdf", StandardFileType.PDF.getName()))); + + // when + indexer.addToIndex(metadata, null); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(2, reader.numDocs()); + } + } +} + 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 new file mode 100644 index 00000000000..4be7e5fc2cd --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java @@ -0,0 +1,106 @@ +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); + 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(database, context); + } + + @Test + public void searchForTest() throws IOException, ParseException { + PdfSearchResults result = search.search("test", 10); + assertEquals(2, 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(2, 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 22b48a39759..f276f9ddfaf 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -1,12 +1,14 @@ package org.jabref.logic.search; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,7 +17,7 @@ public class DatabaseSearcherTest { - public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", true, true); + public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); private BibDatabase database; @@ -26,7 +28,7 @@ public void setUp() { @Test public void testNoMatchesFromEmptyDatabase() { - List matches = new DatabaseSearcher(new SearchQuery("whatever", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -39,7 +41,7 @@ public void testNoMatchesFromEmptyDatabaseWithInvalidSearchExpression() { @Test public void testGetDatabaseFromMatchesDatabaseWithEmptyEntries() { database.insertEntry(new BibEntry()); - List matches = new DatabaseSearcher(new SearchQuery("whatever", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -48,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", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -57,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", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.singletonList(entry), matches); } @Test public void testNoMatchesFromEmptyDatabaseWithInvalidQuery() { - SearchQuery query = new SearchQuery("asdf[", true, true); + SearchQuery query = new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); @@ -76,7 +78,7 @@ public void testCorrectMatchFromDatabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); List matches = new DatabaseSearcher(query, database).getMatches(); assertEquals(Collections.singletonList(entry), matches); @@ -91,7 +93,7 @@ public void testNoMatchesFromDatabaseWithTwoEntries() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.singletonList(entry), databaseSearcher.getMatches()); @@ -103,7 +105,7 @@ public void testNoMatchesFromDabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("asdf", true, true); + SearchQuery query = new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); @@ -114,7 +116,7 @@ public void testNoMatchFromDatabaseWithEmptyEntry() { BibEntry entry = new BibEntry(); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index 2be23fd98f4..4bf01fadb73 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -1,11 +1,14 @@ package org.jabref.logic.search; +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.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.junit.jupiter.api.Test; @@ -17,29 +20,29 @@ public class SearchQueryTest { @Test public void testToString() { - assertEquals("\"asdf\" (case sensitive, regular expression)", new SearchQuery("asdf", true, true).toString()); - assertEquals("\"asdf\" (case insensitive, plain text)", new SearchQuery("asdf", false, false).toString()); + 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", true, false).isContainsBasedSearch()); - assertTrue(new SearchQuery("asdf", true, true).isContainsBasedSearch()); - assertFalse(new SearchQuery("author=asdf", true, false).isContainsBasedSearch()); + 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", true, false).isGrammarBasedSearch()); - assertFalse(new SearchQuery("asdf", true, true).isGrammarBasedSearch()); - assertTrue(new SearchQuery("author=asdf", true, false).isGrammarBasedSearch()); + 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\"", false, false); + SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(entry)); } @@ -47,7 +50,7 @@ public void testGrammarSearch() { public void testGrammarSearchFullEntryLastCharMissing() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "systematic revie"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", false, false); + SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(entry)); } @@ -55,7 +58,7 @@ public void testGrammarSearchFullEntryLastCharMissing() { public void testGrammarSearchFullEntry() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "systematic review"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", false, false); + SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(entry)); } @@ -64,7 +67,7 @@ public void testSearchingForOpenBraketInBooktitle() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.BOOKTITLE, "Super Conference (SC)"); - SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", false, false); + SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -73,7 +76,7 @@ public void testSearchMatchesSingleKeywordNotPart() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anykeyword==apple", false, false); + SearchQuery searchQuery = new SearchQuery("anykeyword==apple", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(e)); } @@ -82,7 +85,7 @@ public void testSearchMatchesSingleKeyword() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", false, false); + SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -92,7 +95,7 @@ public void testSearchAllFields() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -102,7 +105,7 @@ public void testSearchAllFieldsNotForSpecificField() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(e)); } @@ -112,7 +115,7 @@ public void testSearchAllFieldsAndSpecificField() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -122,59 +125,59 @@ public void testIsMatch() { entry.setType(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "asdf"); - assertFalse(new SearchQuery("BiblatexEntryType", true, true).isMatch(entry)); - assertTrue(new SearchQuery("asdf", true, true).isMatch(entry)); - assertTrue(new SearchQuery("author=asdf", true, true).isMatch(entry)); + 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", true, false).isValid()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, false).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsNotValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, true).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryAsRegEx() { - assertTrue(new SearchQuery("asdf", true, true).isValid()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithNumbersAsRegEx() { - assertTrue(new SearchQuery("123", true, true).isValid()); + assertTrue(new SearchQuery("123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryContainsBracketAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, true).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithEqualSignAsRegEx() { - assertTrue(new SearchQuery("author=asdf", true, true).isValid()); + 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", true, true).isValid()); + 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", true, false).isValid()); + assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsValidQueryWithNumbersAndEqualSignNotAsRegEx() { - assertTrue(new SearchQuery("author=123", true, false).isValid()); + assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test @@ -184,21 +187,21 @@ public void isMatchedForNormalAndFieldBasedSearchMixed() { entry.setField(StandardField.AUTHOR, "asdf"); entry.setField(StandardField.ABSTRACT, "text"); - assertTrue(new SearchQuery("text AND author=asdf", true, true).isMatch(entry)); + 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, false, false); + SearchQuery result = new SearchQuery(query, EnumSet.noneOf(SearchFlags.class)); assertFalse(result.isGrammarBasedSearch()); } @Test public void testGetPattern() { String query = "progress"; - SearchQuery result = new SearchQuery(query, false, false); + 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)); @@ -207,7 +210,7 @@ public void testGetPattern() { @Test public void testGetRegexpPattern() { String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, false, true); + 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)); } @@ -215,7 +218,7 @@ public void testGetRegexpPattern() { @Test public void testGetRegexpJavascriptPattern() { String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, false, true); + 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)); } @@ -224,7 +227,7 @@ public void testGetRegexpJavascriptPattern() { public void testEscapingInPattern() { // first word contain all java special regex characters String queryText = "<([{\\\\^-=$!|]})?*+.> word1 word2."; - SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, false, false); + 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)); } @@ -233,7 +236,7 @@ public void testEscapingInPattern() { 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, false, false); + SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, EnumSet.noneOf(SearchFlags.class)); String pattern = "(\\(\\[\\{\\\\\\^\\$\\|\\]\\}\\)\\?\\*\\+\\.\\/)|(word1)|(word2\\.)"; assertEquals(Optional.of(pattern), textQueryWithSpecialChars.getJavaScriptPatternForWords().map(Pattern::toString)); } diff --git a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java index faa4d9d3d56..8878219ecda 100644 --- a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java +++ b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Optional; @@ -11,6 +12,8 @@ import org.jabref.model.entry.field.StandardField; 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; @@ -81,7 +84,7 @@ private static AbstractGroup getKeywordGroup(String name) { } private static AbstractGroup getSearchGroup(String name) { - return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", true, false); + return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); } private static AbstractGroup getExplict(String name) { @@ -253,7 +256,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", false, false); + AbstractGroup newGroup = new SearchGroup("NewGroup", GroupHierarchyType.INDEPENDENT, "test", EnumSet.noneOf(SearchFlags.class)); node.setGroup(newGroup, true, true, entries); @@ -331,7 +334,7 @@ void onlySubgroupsContainAllEntries() { @Test void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", true, false)); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); List fieldChanges = searchGroup.addEntriesToGroup(entries); assertEquals(Collections.emptyList(), fieldChanges); @@ -339,7 +342,7 @@ void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { @Test void removeEntriesFromGroupWorksNotForGroupsNotSupportingExplicitRemovalOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", true, false)); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); 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 bdc24d22b56..9b499ccf6ee 100644 --- a/src/test/java/org/jabref/model/groups/SearchGroupTest.java +++ b/src/test/java/org/jabref/model/groups/SearchGroupTest.java @@ -1,6 +1,9 @@ package org.jabref.model.groups; +import java.util.EnumSet; + import org.jabref.model.entry.BibEntry; +import org.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.Test; @@ -10,7 +13,7 @@ public class SearchGroupTest { @Test public void containsFindsWordWithRegularExpression() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", true, true); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); BibEntry entry = new BibEntry(); entry.addKeyword("review", ','); diff --git a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java index ad7b0f645ad..337263b0dbc 100644 --- a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java +++ b/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java @@ -1,5 +1,7 @@ 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; @@ -17,10 +19,10 @@ public class ContainBasedSearchRuleTest { @Test public void testBasicSearchParsing() { BibEntry be = makeBibtexEntry(); - ContainBasedSearchRule bsCaseSensitive = new ContainBasedSearchRule(true); - ContainBasedSearchRule bsCaseInsensitive = new ContainBasedSearchRule(false); - RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(true); - RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(false); + ContainBasedSearchRule bsCaseSensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + ContainBasedSearchRule bsCaseInsensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); + RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "marine 2001 shields"; diff --git a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java index 28f045b07fb..a32394078ab 100644 --- a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java +++ b/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java @@ -1,5 +1,7 @@ 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; @@ -16,7 +18,7 @@ public class GrammarBasedSearchRuleTest { @Test void applyRuleMatchesSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(true, true); + GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "M[a-z]+e"; assertTrue(searchRule.validateSearchStrings(query)); @@ -25,7 +27,7 @@ void applyRuleMatchesSingleTermWithRegex() { @Test void applyRuleDoesNotMatchSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(true, true); + GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "M[0-9]+e"; assertTrue(searchRule.validateSearchStrings(query)); diff --git a/src/test/resources/.gitignore b/src/test/resources/.gitignore new file mode 100644 index 00000000000..aad78953eb0 --- /dev/null +++ b/src/test/resources/.gitignore @@ -0,0 +1 @@ +luceneTestIndex diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml b/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml index 69b03d0a6dc..65d00f3f7f6 100644 --- a/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml +++ b/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml @@ -1,30 +1,31 @@ - - - Nac16 - Misc - {BD524449-102F-470B-951F-CFE852BA526D} - MeinArtikel - 2016 - 17 - MeineZeitung - - - Nachname, Vorname MIddleName; Nachname2, Vorname2 MiddleName21 - - - - - TestÜbersetzer - - - - - 1 - 07 - 1 - 2018 - 07 - 1 - + + + Nac16 + Misc + {BD524449-102F-470B-951F-CFE852BA526D} + MeinArtikel + 2016 + 17 + MeineZeitung + + + Nachname, Vorname MIddleName; Nachname2, Vorname2 MiddleName21 + + + + + TestÜbersetzer + + + + + 1 + 07 + 1 + 2018 + 07 + 1 + diff --git a/src/test/resources/pdfs/example.pdf b/src/test/resources/pdfs/example.pdf new file mode 100644 index 00000000000..19ae584cebd Binary files /dev/null and b/src/test/resources/pdfs/example.pdf differ diff --git a/src/test/resources/pdfs/metaData.pdf b/src/test/resources/pdfs/metaData.pdf new file mode 100644 index 00000000000..b7ed86bdf9c Binary files /dev/null and b/src/test/resources/pdfs/metaData.pdf differ