net.sourceforge.pmd.util.fxdesigner.XPathPanelController.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.pmd.util.fxdesigner.XPathPanelController.java

Source

/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.util.fxdesigner;

import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.collection.LiveArrayList;
import org.reactfx.util.Tuples;
import org.reactfx.value.Val;
import org.reactfx.value.Var;

import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.lang.rule.xpath.XPathRuleQuery;
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry;
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category;
import net.sourceforge.pmd.util.fxdesigner.model.ObservableXPathRuleBuilder;
import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluationException;
import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluator;
import net.sourceforge.pmd.util.fxdesigner.model.XPathSuggestions;
import net.sourceforge.pmd.util.fxdesigner.popups.ExportXPathWizardController;
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper;
import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner;
import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.SyntaxHighlightingCodeArea;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.XPathSyntaxHighlighter;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ContextMenuWithNoArrows;
import net.sourceforge.pmd.util.fxdesigner.util.controls.PropertyTableView;
import net.sourceforge.pmd.util.fxdesigner.util.controls.XpathViolationListCell;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.control.TitledPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

/**
 * XPath panel controller.
 *
 * @author Clment Fournier
 * @see ExportXPathWizardController
 * @since 6.0.0
 */
public class XPathPanelController implements Initializable, SettingsOwner {

    private static final Duration XPATH_REFRESH_DELAY = Duration.ofMillis(100);
    private final DesignerRoot designerRoot;
    private final MainDesignerController parent;
    private final XPathEvaluator xpathEvaluator = new XPathEvaluator();
    private final ObservableXPathRuleBuilder ruleBuilder = new ObservableXPathRuleBuilder();

    @FXML
    private PropertyTableView propertyTableView;
    @FXML
    private SyntaxHighlightingCodeArea xpathExpressionArea;
    @FXML
    private TitledPane violationsTitledPane;
    @FXML
    private ListView<TextAwareNodeWrapper> xpathResultListView;

    // Actually a child of the main view toolbar, but this controller is responsible for it
    @SuppressWarnings("PMD.SingularField")
    private ChoiceBox<String> xpathVersionChoiceBox;

    public XPathPanelController(DesignerRoot owner, MainDesignerController mainController) {
        this.designerRoot = owner;
        parent = mainController;

        getRuleBuilder().setClazz(XPathRule.class);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        xpathExpressionArea.setSyntaxHighlighter(new XPathSyntaxHighlighter());

        initGenerateXPathFromStackTrace();

        xpathResultListView.setCellFactory(v -> new XpathViolationListCell());

        EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty())
                .conditionOn(xpathResultListView.focusedProperty()).filter(Objects::nonNull)
                .map(TextAwareNodeWrapper::getNode).subscribe(parent::onNodeItemSelected);

        Platform.runLater(this::bindToParent);

        xpathExpressionArea.richChanges().filter(t -> !t.isIdentity()).successionEnds(XPATH_REFRESH_DELAY)
                // Reevaluate XPath anytime the expression or the XPath version changes
                .or(xpathVersionProperty().changes()).subscribe(tick -> parent.refreshXPathResults());

        initialiseAutoCompletion();
    }

    private void initialiseAutoCompletion() {

        EventStream<Integer> changesEventStream = xpathExpressionArea.plainTextChanges().map(characterChanges -> {
            if (characterChanges.getRemoved().length() > 0) {
                return characterChanges.getRemovalEnd() - 1;
            }
            return characterChanges.getInsertionEnd();
        });

        EventStream<Integer> keyCombo = EventStreams.eventsOf(xpathExpressionArea, KeyEvent.KEY_PRESSED)
                .filter(key -> key.isControlDown() && key.getCode().equals(KeyCode.SPACE))
                .map(searchPoint -> xpathExpressionArea.getCaretPosition());

        // captured in the closure
        final ContextMenu autoCompletePopup = new ContextMenuWithNoArrows();
        autoCompletePopup.setId("xpathAutocomplete");
        autoCompletePopup.setHideOnEscape(true);

        EventStreams.merge(keyCombo, changesEventStream).map(searchPoint -> {
            int indexOfSlash = xpathExpressionArea.getText().lastIndexOf("/", searchPoint - 1) + 1;
            String input = xpathExpressionArea.getText();
            if (searchPoint > input.length()) {
                searchPoint = input.length();
            }
            input = input.substring(indexOfSlash, searchPoint);

            return Tuples.t(indexOfSlash, input);
        }).filter(t -> StringUtils.isAlpha(t._2)).subscribe(s -> autoComplete(s._1, s._2, autoCompletePopup));

    }

    private void autoComplete(int slashPosition, String input, ContextMenu autoCompletePopup) {

        XPathSuggestions xPathSuggestions = new XPathSuggestions(parent.getLanguageVersion().getLanguage());
        List<String> suggestions = xPathSuggestions.getXPathSuggestions(input.trim());

        List<CustomMenuItem> resultToDisplay = new ArrayList<>();
        if (!suggestions.isEmpty()) {

            for (int i = 0; i < suggestions.size() && i < 5; i++) {
                final String searchResult = suggestions.get(i);

                Label entryLabel = new Label();
                entryLabel.setGraphic(highlightXPathSuggestion(suggestions.get(i), input));
                entryLabel.setPrefHeight(5);
                CustomMenuItem item = new CustomMenuItem(entryLabel, true);
                resultToDisplay.add(item);

                item.setOnAction(e -> {
                    xpathExpressionArea.replaceText(slashPosition, slashPosition + input.length(), searchResult);
                    autoCompletePopup.hide();
                });
            }
        }
        autoCompletePopup.getItems().setAll(resultToDisplay);

        xpathExpressionArea.getCharacterBoundsOnScreen(slashPosition, slashPosition + input.length()).ifPresent(
                bounds -> autoCompletePopup.show(xpathExpressionArea, bounds.getMinX(), bounds.getMaxY()));
    }

    private static TextFlow highlightXPathSuggestion(String text, String match) {
        int filterIndex = text.toLowerCase(Locale.ROOT).indexOf(match.toLowerCase(Locale.ROOT));

        Text textBefore = new Text(text.substring(0, filterIndex));
        Text textAfter = new Text(text.substring(filterIndex + match.length()));
        Text textFilter = new Text(text.substring(filterIndex, filterIndex + match.length())); //instead of "filter" to keep all "case sensitive"
        textFilter.setFill(Color.ORANGE);
        return new TextFlow(textBefore, textFilter, textAfter);
    }

    private void initGenerateXPathFromStackTrace() {

        ContextMenu menu = new ContextMenu();

        MenuItem item = new MenuItem("Generate from stack trace...");
        item.setOnAction(e -> {
            try {
                Stage popup = new Stage();
                FXMLLoader loader = new FXMLLoader(DesignerUtil.getFxml("generate-xpath-from-stack-trace.fxml"));
                Parent root = loader.load();
                Button button = (Button) loader.getNamespace().get("generateButton");
                TextArea area = (TextArea) loader.getNamespace().get("stackTraceArea");

                ValidationSupport validation = new ValidationSupport();

                validation.registerValidator(area,
                        Validator.createEmptyValidator("The stack trace may not be empty"));
                button.disableProperty().bind(validation.invalidProperty());

                button.setOnAction(f -> {
                    DesignerUtil.stackTraceToXPath(area.getText()).ifPresent(xpathExpressionArea::replaceText);
                    popup.close();
                });

                popup.setScene(new Scene(root));
                popup.initStyle(StageStyle.UTILITY);
                popup.initModality(Modality.WINDOW_MODAL);
                popup.initOwner(designerRoot.getMainStage());
                popup.show();
            } catch (IOException e1) {
                throw new RuntimeException(e1);
            }
        });

        menu.getItems().add(item);

        xpathExpressionArea.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> {
            if (t.getButton() == MouseButton.SECONDARY) {
                menu.show(xpathExpressionArea, t.getScreenX(), t.getScreenY());
            }
        });
    }

    // Binds the underlying rule parameters to the parent UI, disconnecting it from the wizard if need be
    private void bindToParent() {
        DesignerUtil.rewire(getRuleBuilder().languageProperty(),
                Val.map(parent.languageVersionProperty(), LanguageVersion::getLanguage));

        DesignerUtil.rewire(getRuleBuilder().xpathVersionProperty(), parent.xpathVersionProperty());
        DesignerUtil.rewire(getRuleBuilder().xpathExpressionProperty(), xpathExpressionProperty());

        DesignerUtil.rewire(getRuleBuilder().rulePropertiesProperty(), propertyTableView.rulePropertiesProperty(),
                propertyTableView::setRuleProperties);
    }

    public void initialiseVersionChoiceBox(ChoiceBox<String> choiceBox) {
        this.xpathVersionChoiceBox = choiceBox;

        ObservableList<String> versionItems = choiceBox.getItems();
        versionItems.add(XPathRuleQuery.XPATH_1_0);
        versionItems.add(XPathRuleQuery.XPATH_1_0_COMPATIBILITY);
        versionItems.add(XPathRuleQuery.XPATH_2_0);

        xpathVersionChoiceBox.getSelectionModel().select(XPathRuleQuery.XPATH_2_0);
        choiceBox.setConverter(DesignerUtil.stringConverter(s -> "XPath " + s, s -> s.substring(6)));
    }

    /**
     * Evaluate the contents of the XPath expression area
     * on the given compilation unit. This updates the xpath
     * result panel, and can log XPath exceptions to the
     * event log panel.
     *
     * @param compilationUnit The AST root
     * @param version         The language version
     */
    public void evaluateXPath(Node compilationUnit, LanguageVersion version) {

        try {
            String xpath = getXpathExpression();
            if (StringUtils.isBlank(xpath)) {
                invalidateResults(false);
                return;
            }

            ObservableList<Node> results = FXCollections.observableArrayList(xpathEvaluator.evaluateQuery(
                    compilationUnit, version, getXpathVersion(), xpath, ruleBuilder.getRuleProperties()));
            xpathResultListView.setItems(
                    results.stream().map(parent::wrapNode).collect(Collectors.toCollection(LiveArrayList::new)));
            parent.highlightXPathResults(results);
            violationsTitledPane.setText("Matched nodes\t(" + results.size() + ")");
        } catch (XPathEvaluationException e) {
            invalidateResults(true);
            designerRoot.getLogger().logEvent(new LogEntry(e, Category.XPATH_EVALUATION_EXCEPTION));
        }

        xpathResultListView.refresh();

    }

    public List<Node> runXPathQuery(Node compilationUnit, LanguageVersion version, String query)
            throws XPathEvaluationException {
        return xpathEvaluator.evaluateQuery(compilationUnit, version, XPathRuleQuery.XPATH_2_0, query,
                ruleBuilder.getRuleProperties());
    }

    public void invalidateResults(boolean error) {
        xpathResultListView.getItems().clear();
        parent.resetXPathResults();
        violationsTitledPane.setText("Matched nodes" + (error ? "\t(error)" : ""));
    }

    public void showExportXPathToRuleWizard() throws IOException {
        ExportXPathWizardController wizard = new ExportXPathWizardController(xpathExpressionProperty());

        FXMLLoader loader = new FXMLLoader(getClass().getResource("fxml/xpath-export-wizard.fxml"));
        loader.setController(wizard);

        final Stage dialog = new Stage();
        dialog.initOwner(designerRoot.getMainStage());
        dialog.setOnCloseRequest(e -> wizard.shutdown());
        dialog.initModality(Modality.WINDOW_MODAL);

        Parent root = loader.load();
        Scene scene = new Scene(root);
        //stage.setTitle("PMD Rule Designer (v " + PMD.VERSION + ')');
        dialog.setScene(scene);
        dialog.show();
    }

    @PersistentProperty
    public String getXpathExpression() {
        return xpathExpressionArea.getText();
    }

    public void setXpathExpression(String expression) {
        xpathExpressionArea.replaceText(expression);
    }

    public Val<String> xpathExpressionProperty() {
        return Val.wrap(xpathExpressionArea.textProperty());
    }

    @PersistentProperty
    public String getXpathVersion() {
        return getRuleBuilder().getXpathVersion();
    }

    public void setXpathVersion(String xpathVersion) {
        getRuleBuilder().setXpathVersion(xpathVersion);
    }

    public Var<String> xpathVersionProperty() {
        return getRuleBuilder().xpathVersionProperty();
    }

    private ObservableXPathRuleBuilder getRuleBuilder() {
        return ruleBuilder;
    }

    @Override
    public List<SettingsOwner> getChildrenSettingsNodes() {
        return Collections.singletonList(getRuleBuilder());
    }
}