Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add query validation for web search #7809

Merged
merged 18 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/org/jabref/gui/Base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1236,3 +1236,7 @@ TextFlow * {
.fontsizeSpinner{
-fx-pref-width: 5em;
}

.text-field:invalid {
-fx-background-color: rgba(240, 128, 128, 0.5);
}
10 changes: 4 additions & 6 deletions src/main/java/org/jabref/gui/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -1040,11 +1039,10 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) {
}

private void trackOpenNewDatabase(LibraryTab libraryTab) {
Map<String, String> properties = new HashMap<>();
Map<String, Double> measurements = new HashMap<>();
measurements.put("NumberOfEntries", (double) libraryTab.getBibDatabaseContext().getDatabase().getEntryCount());

Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("OpenNewDatabase", properties, measurements));
Globals.getTelemetryClient().ifPresent(client -> client.trackEvent(
"OpenNewDatabase",
Map.of(),
Map.of("NumberOfEntries", (double) libraryTab.getBibDatabaseContext().getDatabase().getEntryCount())));
}

public LibraryTab addTab(BibDatabaseContext databaseContext, boolean raisePanel) {
Expand Down
10 changes: 4 additions & 6 deletions src/main/java/org/jabref/gui/actions/JabRefAction.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.jabref.gui.actions;

import java.util.HashMap;
import java.util.Map;

import javafx.beans.binding.Bindings;
Expand Down Expand Up @@ -77,10 +76,9 @@ private void trackExecute(String actionName) {
}

private void trackUserActionSource(String actionName, Sources source) {
Map<String, String> properties = new HashMap<>();
Map<String, Double> measurements = new HashMap<>();
properties.put("Source", source.toString());

Globals.getTelemetryClient().ifPresent(telemetryClient -> telemetryClient.trackEvent(actionName, properties, measurements));
Globals.getTelemetryClient().ifPresent(telemetryClient -> telemetryClient.trackEvent(
actionName,
Map.of("Source", source.toString()),
Map.of()));
calixtus marked this conversation as resolved.
Show resolved Hide resolved
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/jabref/gui/importer/fetcher/WebSearchPane.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.jabref.gui.importer.fetcher;

import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
Expand All @@ -31,6 +33,8 @@

public class WebSearchPane extends SidePaneComponent {

private static final PseudoClass QUERY_INVALID = PseudoClass.getPseudoClass("invalid");

private final PreferencesService preferences;
private final WebSearchPaneViewModel viewModel;

Expand Down Expand Up @@ -75,6 +79,16 @@ protected Node createContentPane() {
query.getStyleClass().add("searchBar");

viewModel.queryProperty().bind(query.textProperty());
EasyBind.subscribe(viewModel.queryValidationStatus().validProperty(),
valid -> {
if (!valid && viewModel.queryValidationStatus().getHighestMessage().isPresent()) {
query.setTooltip(new Tooltip(viewModel.queryValidationStatus().getHighestMessage().get().getMessage()));
query.pseudoClassStateChanged(QUERY_INVALID, true);
} else {
query.setTooltip(null);
query.pseudoClassStateChanged(QUERY_INVALID, false);
}
});

// Allows to trigger search on pressing enter
query.setOnKeyPressed(event -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jabref.gui.importer.fetcher;

import java.util.Map;
import java.util.SortedSet;

import javafx.beans.property.ListProperty;
Expand All @@ -12,6 +13,7 @@
import javafx.collections.ObservableList;

import org.jabref.gui.DialogService;
import org.jabref.gui.Globals;
import org.jabref.gui.StateManager;
import org.jabref.gui.importer.ImportEntriesDialog;
import org.jabref.gui.util.BackgroundTask;
Expand All @@ -23,6 +25,16 @@
import org.jabref.preferences.PreferencesService;

import com.tobiasdiez.easybind.EasyBind;
import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
import de.saxsys.mvvmfx.utils.validation.ValidationMessage;
import de.saxsys.mvvmfx.utils.validation.ValidationStatus;
import de.saxsys.mvvmfx.utils.validation.Validator;
import org.apache.lucene.queryparser.flexible.core.QueryNodeParseException;
import org.apache.lucene.queryparser.flexible.core.parser.SyntaxParser;
import org.apache.lucene.queryparser.flexible.standard.parser.ParseException;
import org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser;

import static org.jabref.logic.importer.fetcher.transformers.AbstractQueryTransformer.NO_EXPLICIT_FIELD;

public class WebSearchPaneViewModel {

Expand All @@ -32,6 +44,9 @@ public class WebSearchPaneViewModel {
private final DialogService dialogService;
private final StateManager stateManager;

private final Validator searchQueryValidator;
private final SyntaxParser parser = new StandardSyntaxParser();

public WebSearchPaneViewModel(PreferencesService preferencesService, DialogService dialogService, StateManager stateManager) {
this.dialogService = dialogService;
this.stateManager = stateManager;
Expand All @@ -50,6 +65,29 @@ public WebSearchPaneViewModel(PreferencesService preferencesService, DialogServi
int newIndex = fetchers.indexOf(newFetcher);
preferencesService.storeSidePanePreferences(preferencesService.getSidePanePreferences().withWebSearchFetcherSelected(newIndex));
});

searchQueryValidator = new FunctionBasedValidator<>(
query,
queryText -> {
if (StringUtil.isBlank(queryText)) {
// in case user did not enter something, it is treated as valid (to avoid UI WTFs)
return null;
}
try {
parser.parse(queryText, NO_EXPLICIT_FIELD);
return null;
} catch (ParseException e) {
String element = e.currentToken.image;
int position = e.currentToken.beginColumn;
if (element == null) {
return ValidationMessage.error(Localization.lang("Invalid query. Check position %0.", position));
} else {
return ValidationMessage.error(Localization.lang("Invalid query element '%0' at position %1", element, position));
}
} catch (QueryNodeParseException e) {
return ValidationMessage.error("");
}
});
}

public ObservableList<SearchBasedFetcher> getFetchers() {
Expand Down Expand Up @@ -77,25 +115,30 @@ public StringProperty queryProperty() {
}

public void search() {
if (StringUtil.isBlank(getQuery())) {
String query = getQuery().trim();
if (StringUtil.isBlank(query)) {
dialogService.notify(Localization.lang("Please enter a search string"));
return;
}

if (stateManager.getActiveDatabase().isEmpty()) {
dialogService.notify(Localization.lang("Please open or start a new library before searching"));
return;
}

SearchBasedFetcher activeFetcher = getSelectedFetcher();
Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("search", Map.of("fetcher", activeFetcher.getName()), Map.of()));
Copy link
Member

@tobiasdiez tobiasdiez Jun 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here as well, and please no globals...


BackgroundTask<ParserResult> task;
task = BackgroundTask.wrap(() -> new ParserResult(activeFetcher.performSearch(getQuery().trim())))
.withInitialMessage(Localization.lang("Processing %0", getQuery().trim()));
task = BackgroundTask.wrap(() -> new ParserResult(activeFetcher.performSearch(query)))
calixtus marked this conversation as resolved.
Show resolved Hide resolved
.withInitialMessage(Localization.lang("Processing %0", query));
task.onFailure(dialogService::showErrorDialogAndWait);

ImportEntriesDialog dialog = new ImportEntriesDialog(stateManager.getActiveDatabase().get(), task);
dialog.setTitle(activeFetcher.getName());
dialogService.showCustomDialogAndWait(dialog);
}

public ValidationStatus queryValidationStatus() {
return searchQueryValidator.getValidationStatus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ default List<BibEntry> performSearch(String searchQuery) throws FetcherException
if (searchQuery.isBlank()) {
return Collections.emptyList();
}
SyntaxParser parser = new StandardSyntaxParser();

SyntaxParser parser = new StandardSyntaxParser();
QueryNode queryNode;
try {
return this.performSearch(parser.parse(searchQuery, NO_EXPLICIT_FIELD));
queryNode = parser.parse(searchQuery, NO_EXPLICIT_FIELD);
} catch (QueryNodeParseException e) {
throw new FetcherException("An error occurred when parsing the query");
}

return this.performSearch(queryNode);
}
}
2 changes: 2 additions & 0 deletions src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,8 @@ You\ must\ restart\ JabRef\ for\ this\ to\ come\ into\ effect.=You must restart
The\ following\ fetchers\ are\ available\:=The following fetchers are available:
Could\ not\ find\ fetcher\ '%0'=Could not find fetcher '%0'
Running\ query\ '%0'\ with\ fetcher\ '%1'.=Running query '%0' with fetcher '%1'.
Invalid\ query.\ Check\ position\ %0.=Invalid query. Check position %0.
Invalid\ query\ element\ '%0'\ at\ position\ %1=Invalid query element '%0' at position %1

Move\ file=Move file
Rename\ file=Rename file
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jabref.gui.importer.fetcher;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.preferences.PreferencesService;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;

class WebSearchPaneViewModelTest {

private PreferencesService preferencesService;
private DialogService dialogService;
private StateManager stateManager;
private WebSearchPaneViewModel viewModel;

@BeforeEach
void setUp() {
preferencesService = Mockito.mock(PreferencesService.class, RETURNS_DEEP_STUBS);
dialogService = Mockito.mock(DialogService.class);
stateManager = Mockito.mock(StateManager.class);

viewModel = new WebSearchPaneViewModel(preferencesService, dialogService, stateManager);
}

@Test
void queryConsistingOfASingleAndIsNotValid() {
viewModel.queryProperty().setValue("AND");
assertFalse(viewModel.queryValidationStatus().validProperty().getValue());
}

@Test
void falseQueryValidationStatus() {
viewModel.queryProperty().setValue("Miami !Beach AND OR Blue");
assertFalse(viewModel.queryValidationStatus().validProperty().getValue());
}

@Test
void correctQueryValidationStatus() {
viewModel.queryProperty().setValue("Miami AND Beach OR Houston AND Texas");
assertTrue(viewModel.queryValidationStatus().validProperty().getValue());
}

@Test
void notFalseQueryValidationStatus() {
viewModel.queryProperty().setValue("Miami !Beach AND OR Blue");
assertTrue(viewModel.queryValidationStatus().validProperty().not().getValue());
}

@Test
void notCorrectQueryValidationStatus() {
viewModel.queryProperty().setValue("Miami AND Beach OR Houston AND Texas");
assertFalse(viewModel.queryValidationStatus().validProperty().not().getValue());
}
}