Handle XPath errors gracefully

This commit is contained in:
Clément Fournier 2019-02-21 14:07:16 +01:00
parent 00fbcdfc9f
commit fc48f93e92
3 changed files with 63 additions and 17 deletions

View File

@ -165,7 +165,7 @@ public class MainDesignerController extends AbstractController<AbstractControlle
if (root.isPresent()) {
xpathPanelController.evaluateXPath(root.get(), getLanguageVersion());
} else {
xpathPanelController.invalidateResults(true);
xpathPanelController.invalidateResultsExternal(true);
}
}
@ -276,7 +276,7 @@ public class MainDesignerController extends AbstractController<AbstractControlle
*/
public void invalidateAst() {
nodeInfoPanelController.setFocusNode(null);
xpathPanelController.invalidateResults(false);
xpathPanelController.invalidateResultsExternal(false);
getDesignerRoot().getNodeSelectionChannel().pushEvent(this, null);
}

View File

@ -13,11 +13,14 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.kordamp.ikonli.javafx.FontIcon;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableEventStream;
import org.reactfx.collection.LiveArrayList;
@ -55,6 +58,7 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
@ -78,6 +82,8 @@ import javafx.stage.StageStyle;
*/
public class XPathPanelController extends AbstractController<MainDesignerController> implements NodeSelectionSource {
private static final Pattern EXCEPTION_PREFIX_PATTERN = Pattern.compile("(?:(?:\\w+\\.)*\\w+:\\s*)*\\s*(.*)$", Pattern.DOTALL);
private static final String NO_MATCH_MESSAGE = "No match in text";
private static final Duration XPATH_REFRESH_DELAY = Duration.ofMillis(100);
private final XPathEvaluator xpathEvaluator = new XPathEvaluator();
private final ObservableXPathRuleBuilder ruleBuilder = new ObservableXPathRuleBuilder();
@ -135,9 +141,12 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
initNodeSelectionHandling(getDesignerRoot(),
selectionEvents.filter(Objects::nonNull).map(TextAwareNodeWrapper::getNode), false);
violationsTitledPane.titleProperty().bind(currentResults.map(List::size).map(n -> "Matched nodes (" + n + ")"));
}
@Override
protected void afterParentInit() {
bindToParent();
@ -252,7 +261,7 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
try {
String xpath = getXpathExpression();
if (StringUtils.isBlank(xpath)) {
invalidateResults(false);
updateResults(false, false, Collections.emptyList(), "Type an XPath expression to show results");
return;
}
@ -262,13 +271,12 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
getXpathVersion(),
xpath,
ruleBuilder.getRuleProperties()));
xpathResultListView.setItems(results.stream().map(parent::wrapNode).collect(Collectors.toCollection(LiveArrayList::new)));
this.currentResults.setValue(results);
violationsTitledPane.setTitle("Matched nodes (" + results.size() + ")");
updateResults(false, false, results, NO_MATCH_MESSAGE);
// Notify that everything went OK so we can avoid logging very recent exceptions
raiseParsableXPathFlag();
} catch (XPathEvaluationException e) {
invalidateResults(true);
updateResults(true, false, Collections.emptyList(), sanitizeExceptionMessage(e));
logUserException(e, Category.XPATH_EVALUATION_EXCEPTION);
}
@ -280,10 +288,12 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
}
public void invalidateResults(boolean error) {
xpathResultListView.getItems().clear();
this.currentResults.setValue(Collections.emptyList());
violationsTitledPane.setTitle("Matched nodes" + (error ? "\t(error)" : ""));
/**
* Called by the rest of the app.
*/
public void invalidateResultsExternal(boolean error) {
String placeholder = error ? "Compilation unit is invalid" : NO_MATCH_MESSAGE;
updateResults(false, true, Collections.emptyList(), placeholder);
}
@ -356,4 +366,27 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
}
private void updateResults(boolean xpathError,
boolean otherError,
List<Node> results,
String emptyResultsPlaceholder) {
Label emptyLabel = xpathError || otherError
? new Label(emptyResultsPlaceholder, new FontIcon("fas-exclamation"))
: new Label(emptyResultsPlaceholder);
xpathResultListView.setPlaceholder(emptyLabel);
xpathResultListView.setItems(results.stream().map(parent::wrapNode).collect(Collectors.toCollection(LiveArrayList::new)));
this.currentResults.setValue(results);
// only show the error label here when it's an xpath error
expressionTitledPane.errorMessageProperty().setValue(xpathError ? emptyResultsPlaceholder : "");
}
private static String sanitizeExceptionMessage(Throwable exception) {
Matcher matcher = EXCEPTION_PREFIX_PATTERN.matcher(exception.getMessage());
return matcher.matches() ? matcher.group(1) : exception.getMessage();
}
}

View File

@ -4,9 +4,10 @@
package net.sourceforge.pmd.util.fxdesigner.util.controls;
import java.util.Collection;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.kordamp.ikonli.javafx.FontIcon;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
@ -18,6 +19,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToolBar;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
@ -33,10 +35,22 @@ public final class ToolbarTitledPane extends TitledPane {
private final ToolBar toolBar = new ToolBar();
private final Var<String> title = Var.newSimpleVar("Title");
private final Var<String> errorMessage = Var.newSimpleVar("");
public ToolbarTitledPane() {
Label errorLabel = new Label();
FontIcon errorIcon = new FontIcon("fas-exclamation-triangle");
errorLabel.setGraphic(errorIcon);
errorLabel.tooltipProperty().bind(errorMessage.map(message -> StringUtils.isBlank(message) ? null : new Tooltip(message)));
errorLabel.visibleProperty().bind(errorMessage.map(StringUtils::isNotBlank));
// makes the label zero-width when it's not visible
errorLabel.managedProperty().bind(errorLabel.visibleProperty());
toolBar.getItems().add(errorLabel);
getStyleClass().add("tool-bar-title");
// change the default
@ -79,11 +93,6 @@ public final class ToolbarTitledPane extends TitledPane {
}
public void setToolbarItems(Collection<? extends Node> nodes) {
toolBar.getItems().setAll(nodes);
}
public String getTitle() {
return title.getValue();
}
@ -94,6 +103,10 @@ public final class ToolbarTitledPane extends TitledPane {
}
public Var<String> errorMessageProperty() {
return errorMessage;
}
/** Title of the pane, not equivalent to {@link #textProperty()}. */
public Var<String> titleProperty() {
return title;