diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 89f8f54568..c9f6b7b739 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,6 +19,13 @@ This is a minor release. ### New and noteworthy +#### Designer UI + +The Designer now supports configuring properties for XPath based rule development. +The Designer is still under development and any feedback is welcome. + +You can start the designer via `run.sh designer` or `designer.bat`. + ### Fixed Issues * all diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java index 99063e45ca..af015a2932 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java @@ -25,7 +25,7 @@ import net.sourceforge.pmd.properties.PropertyDescriptor; * @author Clément Fournier * @since 6.0.0 */ -/* default */ class RuleBuilder { +public class RuleBuilder { private List> definedProperties = new ArrayList<>(); private String name; @@ -44,7 +44,7 @@ import net.sourceforge.pmd.properties.PropertyDescriptor; private boolean isUsesMultifile; private boolean isUsesTyperesolution; - /* default */ RuleBuilder(String name, String clazz, String language) { + public RuleBuilder(String name, String clazz, String language) { this.name = name; language(language); className(clazz); diff --git a/pmd-ui/pom.xml b/pmd-ui/pom.xml index 20a75bf895..e30b07a8f0 100644 --- a/pmd-ui/pom.xml +++ b/pmd-ui/pom.xml @@ -45,6 +45,16 @@ richtextfx 0.8.1 + + org.controlsfx + controlsfx + 8.40.13 + + + commons-beanutils + commons-beanutils-core + 1.8.3 + net.sourceforge.pmd pmd-apex diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/Designer.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/Designer.java index a22e6f8286..20b6a3b8cd 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/Designer.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/Designer.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.stream.Collectors; import net.sourceforge.pmd.PMDVersion; +import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; import javafx.application.Application; import javafx.collections.ObservableList; @@ -47,7 +48,7 @@ public class Designer extends Application { parseParameters(getParameters()); FXMLLoader loader - = new FXMLLoader(getClass().getResource("fxml/designer.fxml")); + = new FXMLLoader(DesignerUtil.getFxml("designer.fxml")); DesignerRoot owner = new DesignerRoot(stage); MainDesignerController mainController = new MainDesignerController(owner); diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EditPropertyDialogController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EditPropertyDialogController.java new file mode 100644 index 0000000000..bfcdb8a5f9 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EditPropertyDialogController.java @@ -0,0 +1,240 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner; + +import static net.sourceforge.pmd.properties.MultiValuePropertyDescriptor.DEFAULT_DELIMITER; +import static net.sourceforge.pmd.properties.MultiValuePropertyDescriptor.DEFAULT_NUMERIC_DELIMITER; +import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.rewire; + +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; + +import org.controlsfx.validation.Severity; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.reactfx.util.Try; +import org.reactfx.value.Var; + +import net.sourceforge.pmd.properties.PropertyTypeId; +import net.sourceforge.pmd.properties.ValueParser; +import net.sourceforge.pmd.properties.ValueParserConstants; +import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; +import net.sourceforge.pmd.util.fxdesigner.util.PropertyDescriptorSpec; +import net.sourceforge.pmd.util.fxdesigner.util.controls.PropertyTableView; + +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.TextField; +import javafx.stage.Stage; + + +/** + * Property edition dialog. Use {@link #bindToDescriptor(PropertyDescriptorSpec, ObservableList)} )} + * to use this dialog to edit a descriptor spec. Typically owned by a {@link PropertyTableView}. + * The controller must be instantiated by hand. + * + * @author Clément Fournier + * @see PropertyDescriptorSpec + * @since 6.0.0 + */ +public class EditPropertyDialogController implements Initializable { + + private final Var typeId = Var.newSimpleVar(PropertyTypeId.STRING); + private final Var commitHandler = Var.newSimpleVar(null); + private Var backingDescriptor = Var.newSimpleVar(null); + private Var> backingDescriptorList = Var.newSimpleVar(null); + + private ValidationSupport validationSupport = new ValidationSupport(); + @FXML + private TextField nameField; + @FXML + private TextField descriptionField; + @FXML + private ChoiceBox typeChoiceBox; + @FXML + private TextField valueField; + @FXML + private Button commitButton; + + + public EditPropertyDialogController() { + + } + + + public EditPropertyDialogController(Runnable commitHandler) { + this.commitHandler.setValue(commitHandler); + } + + + @Override + public void initialize(URL location, ResourceBundle resources) { + + commitButton.setOnAction(e -> { + commitHandler.ifPresent(Runnable::run); + getStage().close(); + this.free(); + }); + + commitButton.disableProperty().bind(validationSupport.invalidProperty()); + + Platform.runLater(() -> { + typeId.bind(typeChoiceBox.getSelectionModel().selectedItemProperty()); + typeChoiceBox.setConverter(DesignerUtil.stringConverter(PropertyTypeId::getStringId, + PropertyTypeId::lookupMnemonic)); + typeChoiceBox.getItems().addAll(PropertyTypeId.typeIdsToConstants().values()); + FXCollections.sort(typeChoiceBox.getItems()); + }); + + Platform.runLater(this::registerBasicValidators); + + typeIdProperty().values() + .filter(Objects::nonNull) + .subscribe(this::registerTypeDependentValidators); + + } + + + private Stage getStage() { + return (Stage) commitButton.getScene().getWindow(); + } + + + /** Unbinds this dialog from its backing properties. */ + public void free() { + backingDescriptor.ifPresent(PropertyDescriptorSpec::unbind); + backingDescriptor.setValue(null); + backingDescriptorList.setValue(null); + } + + + /** + * Wires this dialog to the descriptor, so that the controls edit the descriptor. + * + * @param spec The descriptor + */ + public void bindToDescriptor(PropertyDescriptorSpec spec, ObservableList allDescriptors) { + backingDescriptor.setValue(spec); + backingDescriptorList.setValue(allDescriptors); + rewire(spec.nameProperty(), this.nameProperty(), this::setName); + rewire(spec.typeIdProperty(), this.typeIdProperty(), this::setTypeId); + rewire(spec.valueProperty(), this.valueProperty(), this::setValue); + rewire(spec.descriptionProperty(), this.descriptionProperty(), this::setDescription); + } + + + // Validators for attributes common to all properties + private void registerBasicValidators() { + Validator noWhitespaceName + = Validator.createRegexValidator("Name cannot contain whitespace", "\\S*+", Severity.ERROR); + Validator emptyName = Validator.createEmptyValidator("Name required"); + Validator uniqueName = (c, val) -> { + long sameNameDescriptors = backingDescriptorList.getOrElse(FXCollections.emptyObservableList()) + .stream() + .map(PropertyDescriptorSpec::getName) + .filter(getName()::equals) + .count(); + + return new ValidationResult().addErrorIf(c, "The name must be unique", sameNameDescriptors > 1); + }; + + validationSupport.registerValidator(nameField, Validator.combine(noWhitespaceName, emptyName, uniqueName)); + + Validator noWhitespaceDescription + = Validator.createRegexValidator("Message cannot be whitespace", "(\\s*+\\S.*)?", Severity.ERROR); + Validator emptyDescription = Validator.createEmptyValidator("Message required"); + validationSupport.registerValidator(descriptionField, Validator.combine(noWhitespaceDescription, emptyDescription)); + } + + + private void registerTypeDependentValidators(PropertyTypeId typeId) { + Validator valueValidator = (c, val) -> + ValidationResult.fromErrorIf(valueField, "The value couldn't be parsed", + Try.tryGet(() -> getValueParser(typeId).valueOf(getValue())).isFailure()); + + + validationSupport.registerValidator(valueField, valueValidator); + } + + + private ValueParser getValueParser(PropertyTypeId typeId) { + ValueParser parser = typeId.getValueParser(); + if (typeId.isPropertyMultivalue()) { + char delimiter = typeId.isPropertyNumeric() ? DEFAULT_NUMERIC_DELIMITER : DEFAULT_DELIMITER; + parser = ValueParserConstants.multi(parser, delimiter); + } + return parser; + } + + + public String getName() { + return nameField.getText(); + } + + + public void setName(String name) { + nameField.setText(name); + } + + + public Property nameProperty() { + return nameField.textProperty(); + } + + + public String getDescription() { + return descriptionField.getText(); + } + + + public void setDescription(String description) { + descriptionField.setText(description); + } + + + public Property descriptionProperty() { + return descriptionField.textProperty(); + } + + + public PropertyTypeId getTypeId() { + return typeId.getValue(); + } + + + public void setTypeId(PropertyTypeId typeId) { + typeChoiceBox.getSelectionModel().select(typeId); + } + + + public Var typeIdProperty() { + return typeId; + } + + + public String getValue() { + return valueField.getText(); + } + + + public void setValue(String value) { + valueField.setText(value); + } + + + public Property valueProperty() { + return valueField.textProperty(); + } + + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EventLogController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EventLogController.java index 3f53190522..e4ab58c36c 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EventLogController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EventLogController.java @@ -23,6 +23,7 @@ import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.cell.PropertyValueFactory; + /** * @author Clément Fournier * @since 6.0.0 diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/ExportXPathWizardController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/ExportXPathWizardController.java index f8d4aaa34b..51570ac3f6 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/ExportXPathWizardController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/ExportXPathWizardController.java @@ -5,7 +5,6 @@ package net.sourceforge.pmd.util.fxdesigner; import java.net.URL; -import java.util.Arrays; import java.util.Map; import java.util.ResourceBundle; import java.util.WeakHashMap; @@ -58,10 +57,11 @@ public class ExportXPathWizardController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { - languageChoiceBox.getItems().addAll(Arrays.stream(DesignerUtil.getSupportedLanguageVersions()) - .map(LanguageVersion::getLanguage) - .distinct() - .collect(Collectors.toList())); + languageChoiceBox.getItems().addAll(DesignerUtil.getSupportedLanguageVersions() + .stream() + .map(LanguageVersion::getLanguage) + .distinct() + .collect(Collectors.toList())); languageChoiceBox.setConverter(new StringConverter() { @Override @@ -117,34 +117,34 @@ public class ExportXPathWizardController implements Initializable { // TODO very inefficient, can we do better? final String template = " >\n" - + " \n" - + "%s\n" - + " \n" - + " %d\n" - + " \n" - + " \n" - + " \n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; + + " language=\"%s\"\n" + + " message=\"%s\"\n" + + " class=\"net.sourceforge.pmd.lang.rule.XPathRule\"\n" + + " >\n" + + " \n" + + "%s\n" + + " \n" + + " %d\n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; return String.format(template, - nameField.getText(), - languageChoiceBox.getSelectionModel().getSelectedItem().getTerseName(), - messageField.getText(), - "TODO", - descriptionField.getText(), // TODO format - (int) prioritySlider.getValue(), - xpathExpression.getValue() + nameField.getText(), + languageChoiceBox.getSelectionModel().getSelectedItem().getTerseName(), + messageField.getText(), + "TODO", + descriptionField.getText(), // TODO format + (int) prioritySlider.getValue(), + xpathExpression.getValue() ); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java index c56d49b13d..d0b0d62078 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java @@ -12,24 +12,22 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.ResourceBundle; import java.util.Stack; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; +import org.reactfx.value.Val; -import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.symboltable.NameDeclaration; import net.sourceforge.pmd.lang.symboltable.NameOccurrence; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; import net.sourceforge.pmd.util.fxdesigner.util.LimitedSizeStack; -import net.sourceforge.pmd.util.fxdesigner.util.settings.AppSetting; -import net.sourceforge.pmd.util.fxdesigner.util.settings.SettingsOwner; -import net.sourceforge.pmd.util.fxdesigner.util.settings.XMLSettingsLoader; -import net.sourceforge.pmd.util.fxdesigner.util.settings.XMLSettingsSaver; +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner; +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil; +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -37,10 +35,7 @@ import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Parent; -import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; @@ -58,10 +53,7 @@ import javafx.scene.control.TextArea; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.stage.FileChooser; -import javafx.stage.Modality; -import javafx.stage.Stage; import javafx.util.Duration; -import javafx.util.StringConverter; /** @@ -93,8 +85,6 @@ public class MainDesignerController implements Initializable, SettingsOwner { @FXML private MenuItem exportXPathMenuItem; @FXML - private Menu exportMenu; - @FXML private Menu fileMenu; /* Center toolbar */ @FXML @@ -123,7 +113,12 @@ public class MainDesignerController implements Initializable, SettingsOwner { private SourceEditorController sourceEditorController; @FXML private EventLogController eventLogPanelController; + + // Other fields private Stack recentFiles = new LimitedSizeStack<>(5); + // Properties + private Val languageVersion = Val.constant(DesignerUtil.defaultLanguageVersion()); + private Val xpathVersion = Val.constant(DesignerUtil.defaultXPathVersion()); public MainDesignerController(DesignerRoot owner) { @@ -133,22 +128,26 @@ public class MainDesignerController implements Initializable, SettingsOwner { @Override public void initialize(URL location, ResourceBundle resources) { - try { - XMLSettingsLoader loader = new XMLSettingsLoader(DesignerUtil.getSettingsFile()); - loadSettings(loader.getSettings()); - } catch (IOException ioe) { - // no big deal + SettingsPersistenceUtil.restoreProperties(this, DesignerUtil.getSettingsFile()); + } catch (Exception e) { + // shouldn't prevent the app from opening + // in case the file is corrupted, it will be overwritten on shutdown + e.printStackTrace(); } - initializeLanguageVersionMenu(); initializeViewAnimation(); xpathPanelController.initialiseVersionChoiceBox(xpathVersionChoiceBox); - sourceEditorController.languageVersionProperty().bind(languageChoiceBox.getSelectionModel().selectedItemProperty()); - xpathPanelController.xpathVersionProperty().bind(xpathVersionChoiceBox.getSelectionModel().selectedItemProperty()); + languageVersion = Val.wrap(languageChoiceBox.getSelectionModel().selectedItemProperty()); + DesignerUtil.rewire(sourceEditorController.languageVersionProperty(), + languageVersion, this::setLanguageVersion); + + xpathVersion = Val.wrap(xpathVersionChoiceBox.getSelectionModel().selectedItemProperty()); + DesignerUtil.rewire(xpathPanelController.xpathVersionProperty(), + xpathVersion, this::setXpathVersion); refreshASTButton.setOnAction(e -> onRefreshASTClicked()); licenseMenuItem.setOnAction(e -> showLicensePopup()); @@ -158,41 +157,32 @@ public class MainDesignerController implements Initializable, SettingsOwner { fileMenu.setOnShowing(e -> onFileMenuShowing()); exportXPathMenuItem.setOnAction(e -> { try { - onExportXPathToRuleClicked(); - } catch (IOException ex) { - ex.printStackTrace(); - // pretend it didn't happen + xpathPanelController.showExportXPathToRuleWizard(); + } catch (IOException e1) { + e1.printStackTrace(); } }); sourceEditorController.refreshAST(); + xpathPanelController.evaluateXPath(sourceEditorController.getCompilationUnit(), + getLanguageVersion()); Platform.runLater(() -> sourceEditorController.moveCaret(0, 0)); + Platform.runLater(() -> { // fixes choicebox bad rendering on first opening + languageChoiceBox.show(); + languageChoiceBox.hide(); + }); } private void initializeLanguageVersionMenu() { - List supported = Arrays.asList(DesignerUtil.getSupportedLanguageVersions()); + List supported = DesignerUtil.getSupportedLanguageVersions(); supported.sort(LanguageVersion::compareTo); languageChoiceBox.getItems().addAll(supported); + languageChoiceBox.setConverter(DesignerUtil.languageVersionStringConverter()); - languageChoiceBox.setConverter(new StringConverter() { - @Override - public String toString(LanguageVersion object) { - return object.getShortName(); - } - - - @Override - public LanguageVersion fromString(String string) { - return LanguageRegistry.findLanguageVersionByTerseName(string.toLowerCase()); - } - }); - - LanguageVersion defaultLangVersion = LanguageRegistry.getLanguage("Java").getDefaultVersion(); - languageChoiceBox.getSelectionModel().select(defaultLangVersion); + languageChoiceBox.getSelectionModel().select(DesignerUtil.defaultLanguageVersion()); languageChoiceBox.show(); - } @@ -200,7 +190,7 @@ public class MainDesignerController implements Initializable, SettingsOwner { // gets captured in the closure final double defaultMainHorizontalSplitPaneDividerPosition - = mainHorizontalSplitPane.getDividerPositions()[0]; + = mainHorizontalSplitPane.getDividerPositions()[0]; // show/ hide bottom pane @@ -223,9 +213,7 @@ public class MainDesignerController implements Initializable, SettingsOwner { public void shutdown() { try { - XMLSettingsSaver saver = XMLSettingsSaver.forFile(DesignerUtil.getSettingsFile()); - this.saveSettings(saver); - saver.save(); + SettingsPersistenceUtil.persistProperties(this, DesignerUtil.getSettingsFile()); } catch (IOException ioe) { // nevermind } @@ -238,7 +226,7 @@ public class MainDesignerController implements Initializable, SettingsOwner { private void onRefreshASTClicked() { sourceEditorController.refreshAST(); xpathPanelController.evaluateXPath(sourceEditorController.getCompilationUnit(), - sourceEditorController.getLanguageVersion()); + getLanguageVersion()); } @@ -255,13 +243,15 @@ public class MainDesignerController implements Initializable, SettingsOwner { public void onNameDeclarationSelected(NameDeclaration declaration) { sourceEditorController.clearNodeHighlight(); + + List occ = declaration.getNode().getScope().getDeclarations().get(declaration); + if (occ != null) { + sourceEditorController.highlightNodesSecondary(occ.stream() + .map(NameOccurrence::getLocation) + .collect(Collectors.toList())); + } + sourceEditorController.highlightNodePrimary(declaration.getNode()); - sourceEditorController.highlightNodesSecondary(declaration.getNode().getScope() - .getDeclarations() - .get(declaration) - .stream() - .map(NameOccurrence::getLocation) - .collect(Collectors.toList())); } @@ -282,27 +272,6 @@ public class MainDesignerController implements Initializable, SettingsOwner { } - private void onExportXPathToRuleClicked() throws IOException { - // doesn't work for some reason - ExportXPathWizardController wizard - = new ExportXPathWizardController(xpathPanelController.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(); - } - - private void onFileMenuShowing() { openRecentMenu.setDisable(recentFiles.isEmpty()); } @@ -316,15 +285,15 @@ public class MainDesignerController implements Initializable, SettingsOwner { sourceEditorController.clearStyleLayers(); } - private void loadSourceFromFile(File file) { if (file != null) { try { String source = IOUtils.toString(new FileInputStream(file)); - sourceEditorController.replaceText(source); + sourceEditorController.setText(source); LanguageVersion guess = DesignerUtil.getLanguageVersionFromExtension(file.getName()); if (guess != null) { // guess the language from the extension languageChoiceBox.getSelectionModel().select(guess); + onRefreshASTClicked(); } recentFiles.push(file); @@ -337,6 +306,7 @@ public class MainDesignerController implements Initializable, SettingsOwner { private void updateRecentFilesMenu() { List items = new ArrayList<>(); + List filesToClear = new ArrayList<>(); for (final File f : recentFiles) { if (f.exists() && f.isFile()) { @@ -346,9 +316,11 @@ public class MainDesignerController implements Initializable, SettingsOwner { Tooltip.install(item.getContent(), new Tooltip(f.getAbsolutePath())); items.add(item); } else { - recentFiles.remove(f); + filesToClear.add(f); } } + recentFiles.removeAll(filesToClear); + if (items.isEmpty()) { openRecentMenu.setDisable(true); return; @@ -369,48 +341,49 @@ public class MainDesignerController implements Initializable, SettingsOwner { } - @Override - public List getSettings() { - List settings = new ArrayList<>(); - settings.add(new AppSetting("recentFiles", this::getRecentFiles, this::setRecentFiles)); - settings.add(new AppSetting("isMaximized", this::isMaximized, this::setIsMaximized)); - settings.add(new AppSetting("bottomExpandedTab", this::getBottomExpandedTab, this::setBottomExpandedTab)); - return settings; + public void invalidateAst() { + nodeInfoPanelController.invalidateInfo(); + xpathPanelController.invalidateResults(false); + sourceEditorController.clearNodeHighlight(); } - private void saveSettings(XMLSettingsSaver saver) { - saveSettingsOf(this, saver); - saveSettingsOf(sourceEditorController, saver); - saveSettingsOf(xpathPanelController, saver); + public LanguageVersion getLanguageVersion() { + return languageVersion.getValue(); } - private void saveSettingsOf(SettingsOwner owner, XMLSettingsSaver saver) { - for (AppSetting s : owner.getSettings()) { - saver.put(s.getKeyName(), s.getValue()); + public void setLanguageVersion(LanguageVersion version) { + if (languageChoiceBox.getItems().contains(version)) { + languageChoiceBox.getSelectionModel().select(version); } } - private void loadSettings(Map settings) { - loadSettingsOf(sourceEditorController, settings); - loadSettingsOf(xpathPanelController, settings); - loadSettingsOf(this, settings); + public Val languageVersionProperty() { + return languageVersion; } - private void loadSettingsOf(SettingsOwner owner, Map loaded) { - for (AppSetting s : owner.getSettings()) { - String val = loaded.get(s.getKeyName()); - if (val != null) { - s.setValue(val); - } + public String getXpathVersion() { + return xpathVersion.getValue(); + } + + + public void setXpathVersion(String version) { + if (xpathVersionChoiceBox.getItems().contains(version)) { + xpathVersionChoiceBox.getSelectionModel().select(version); } } - private String getRecentFiles() { + public Val xpathVersionProperty() { + return xpathVersion; + } + + + @PersistentProperty + public String getRecentFiles() { StringBuilder sb = new StringBuilder(); for (File f : recentFiles) { sb.append(',').append(f.getAbsolutePath()); @@ -419,7 +392,7 @@ public class MainDesignerController implements Initializable, SettingsOwner { } - private void setRecentFiles(String files) { + public void setRecentFiles(String files) { List fileNames = Arrays.asList(files.split(",")); Collections.reverse(fileNames); for (String fileName : fileNames) { @@ -429,34 +402,42 @@ public class MainDesignerController implements Initializable, SettingsOwner { } - private String isMaximized() { - return Boolean.toString(designerRoot.getMainStage().isMaximized()); + @PersistentProperty + public boolean isMaximized() { + return designerRoot.getMainStage().isMaximized(); } - private void setIsMaximized(String bool) { - boolean b = Boolean.parseBoolean(bool); + public void setMaximized(boolean b) { designerRoot.getMainStage().setMaximized(!b); // trigger change listener anyway designerRoot.getMainStage().setMaximized(b); } - private String getBottomExpandedTab() { - return (bottomTabsToggle.isSelected() ? "expanded:" : "collapsed:") - + bottomTabPane.getSelectionModel().getSelectedIndex(); + @PersistentProperty + public boolean isBottomTabExpanded() { + return bottomTabsToggle.isSelected(); } - private void setBottomExpandedTab(String id) { - String[] info = id.split(":"); - bottomTabsToggle.setSelected("expanded".equals(info[0])); - bottomTabPane.getSelectionModel().select(Integer.parseInt(info[1])); + public void setBottomTabExpanded(boolean b) { + bottomTabsToggle.setSelected(b); } - public void invalidateAst() { - nodeInfoPanelController.invalidateInfo(); - xpathPanelController.invalidateResults(false); - sourceEditorController.clearNodeHighlight(); + @PersistentProperty + public int getBottomTabIndex() { + return bottomTabPane.getSelectionModel().getSelectedIndex(); + } + + + public void setBottomTabIndex(int i) { + bottomTabPane.getSelectionModel().select(i); + } + + + @Override + public List getChildrenSettingsNodes() { + return Arrays.asList(xpathPanelController, sourceEditorController); } } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java index 904162b826..0205fb36d5 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java @@ -9,6 +9,8 @@ import java.util.Collections; import java.util.Objects; import java.util.ResourceBundle; +import org.reactfx.EventStreams; + import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.ast.xpath.Attribute; import net.sourceforge.pmd.lang.ast.xpath.AttributeAxisIterator; @@ -16,6 +18,7 @@ import net.sourceforge.pmd.lang.java.ast.TypeNode; import net.sourceforge.pmd.lang.symboltable.NameDeclaration; import net.sourceforge.pmd.util.fxdesigner.model.MetricEvaluator; import net.sourceforge.pmd.util.fxdesigner.model.MetricResult; +import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeCell; import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeItem; import javafx.collections.FXCollections; @@ -58,7 +61,7 @@ public class NodeInfoPanelController implements Initializable { private MetricEvaluator metricEvaluator = new MetricEvaluator(); - NodeInfoPanelController(DesignerRoot root, MainDesignerController mainController) { + public NodeInfoPanelController(DesignerRoot root, MainDesignerController mainController) { this.designerRoot = root; parent = mainController; } @@ -66,11 +69,13 @@ public class NodeInfoPanelController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { - scopeHierarchyTreeView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null && newVal.getValue() instanceof NameDeclaration) { - parent.onNameDeclarationSelected((NameDeclaration) newVal.getValue()); - } - }); + EventStreams.valuesOf(scopeHierarchyTreeView.getSelectionModel().selectedItemProperty()) + .filter(Objects::nonNull) + .map(TreeItem::getValue) + .filterMap(o -> o instanceof NameDeclaration, o -> (NameDeclaration) o) + .subscribe(parent::onNameDeclarationSelected); + + scopeHierarchyTreeView.setCellFactory(view -> new ScopeHierarchyTreeCell(parent)); } @@ -93,7 +98,8 @@ public class NodeInfoPanelController implements Initializable { .filter(result -> !result.isNaN()) .count()); - + // TODO maybe a better way would be to build all the scope TreeItem hierarchy once + // and only expand the ascendants of the node. TreeItem rootScope = ScopeHierarchyTreeItem.buildAscendantHierarchy(node); scopeHierarchyTreeView.setRoot(rootScope); } @@ -134,7 +140,7 @@ public class NodeInfoPanelController implements Initializable { while (attributeAxisIterator.hasNext()) { Attribute attribute = attributeAxisIterator.next(); result.add(attribute.getName() + " = " - + ((attribute.getValue() != null) ? attribute.getStringValue() : "null")); + + ((attribute.getValue() != null) ? attribute.getStringValue() : "null")); } if (node instanceof TypeNode) { diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java index ae653a0b6a..c6c71c98a6 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java @@ -5,32 +5,31 @@ package net.sourceforge.pmd.util.fxdesigner; import java.net.URL; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.fxmisc.richtext.LineNumberFactory; +import org.reactfx.EventStreams; +import org.reactfx.value.Val; +import org.reactfx.value.Var; -import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.util.fxdesigner.model.ASTManager; import net.sourceforge.pmd.util.fxdesigner.model.ParseAbortedException; +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.AvailableSyntaxHighlighters; import net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea; import net.sourceforge.pmd.util.fxdesigner.util.codearea.SyntaxHighlighter; import net.sourceforge.pmd.util.fxdesigner.util.controls.ASTTreeItem; -import net.sourceforge.pmd.util.fxdesigner.util.settings.AppSetting; -import net.sourceforge.pmd.util.fxdesigner.util.settings.SettingsOwner; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Label; @@ -56,7 +55,7 @@ public class SourceEditorController implements Initializable, SettingsOwner { private TreeView astTreeView; @FXML private CustomCodeArea codeEditorArea; - private BooleanProperty isSyntaxHighlightingEnabled = new SimpleBooleanProperty(true); + private ASTManager astManager; @@ -69,51 +68,32 @@ public class SourceEditorController implements Initializable, SettingsOwner { @Override public void initialize(URL location, ResourceBundle resources) { - initializeSyntaxHighlighting(); - initializeASTTreeView(); + languageVersionProperty().values() + .filterMap(Objects::nonNull, LanguageVersion::getLanguage) + .distinct() + .subscribe(this::updateSyntaxHighlighter); + + EventStreams.valuesOf(astTreeView.getSelectionModel().selectedItemProperty()) + .filterMap(Objects::nonNull, TreeItem::getValue) + .subscribe(parent::onNodeItemSelected); codeEditorArea.setParagraphGraphicFactory(LineNumberFactory.get(codeEditorArea)); } - private void initializeSyntaxHighlighting() { - - isSyntaxHighlightingEnabled.bind(codeEditorArea.syntaxHighlightingEnabledProperty()); - - isSyntaxHighlightingEnabled.addListener(((observable, wasEnabled, isEnabled) -> { - if (!wasEnabled && isEnabled) { - updateSyntaxHighlighter(); - } else if (!isEnabled) { - codeEditorArea.disableSyntaxHighlighting(); - } - })); - - astManager.languageVersionProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null && !newVal.equals(oldVal)) { - updateSyntaxHighlighter(); - } - }); - - } - - - private void initializeASTTreeView() { - - astTreeView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null && newVal.getValue() != null) { - parent.onNodeItemSelected(newVal.getValue()); - } - }); - } - - /** * Refreshes the AST. */ public void refreshAST() { - String source = codeEditorArea.getText(); - Node previous = astManager.compilationUnitProperty().get(); + String source = getText(); + Node previous = getCompilationUnit(); Node current; + + if (StringUtils.isBlank(source)) { + astTreeView.setRoot(null); + return; + } + try { current = astManager.updateCompilationUnit(source); } catch (ParseAbortedException e) { @@ -122,11 +102,8 @@ public class SourceEditorController implements Initializable, SettingsOwner { } if (previous != current) { parent.invalidateAst(); + setUpToDateCompilationUnit(current); } - - setUpToDateCompilationUnit(current); - codeEditorArea.clearPrimaryStyleLayer(); - } @@ -142,10 +119,11 @@ public class SourceEditorController implements Initializable, SettingsOwner { } - private void updateSyntaxHighlighter() { - SyntaxHighlighter computer = AvailableSyntaxHighlighters.getComputerForLanguage(astManager.getLanguageVersion().getLanguage()); - if (computer != null) { - codeEditorArea.setSyntaxHighlightingEnabled(computer); + private void updateSyntaxHighlighter(Language language) { + Optional highlighter = AvailableSyntaxHighlighters.getHighlighterForLanguage(language); + + if (highlighter.isPresent()) { + codeEditorArea.setSyntaxHighlightingEnabled(highlighter.get()); } else { codeEditorArea.disableSyntaxHighlighting(); } @@ -162,7 +140,7 @@ public class SourceEditorController implements Initializable, SettingsOwner { } - private void highlightNodes(Collection nodes, Set cssClasses) { + private void highlightNodes(Collection nodes, Set cssClasses) { for (Node node : nodes) { if (codeEditorArea.isInRange(node)) { codeEditorArea.styleCss(node, cssClasses); @@ -176,7 +154,7 @@ public class SourceEditorController implements Initializable, SettingsOwner { } - public void highlightNodesSecondary(Collection nodes) { + public void highlightNodesSecondary(Collection nodes) { highlightNodes(nodes, Collections.singleton("secondary-highlight")); } @@ -202,44 +180,45 @@ public class SourceEditorController implements Initializable, SettingsOwner { codeEditorArea.requestFollowCaret(); } - - public boolean isSyntaxHighlightingEnabled() { - return isSyntaxHighlightingEnabled.get(); - } - - - public ReadOnlyBooleanProperty syntaxHighlightingEnabledProperty() { - return isSyntaxHighlightingEnabled; - } - - - public ObservableValue sourceCodeProperty() { - return codeEditorArea.textProperty(); - } - - + @PersistentProperty public LanguageVersion getLanguageVersion() { return astManager.getLanguageVersion(); } - public ObjectProperty languageVersionProperty() { + public void setLanguageVersion(LanguageVersion version) { + astManager.setLanguageVersion(version); + } + + + public Var languageVersionProperty() { return astManager.languageVersionProperty(); } public Node getCompilationUnit() { - return astManager.updateCompilationUnit(); + return astManager.getCompilationUnit(); } - public ObjectProperty compilationUnitProperty() { + public Val compilationUnitProperty() { return astManager.compilationUnitProperty(); } - public void replaceText(String source) { - codeEditorArea.replaceText(source); + @PersistentProperty + public String getText() { + return codeEditorArea.getText(); + } + + + public void setText(String expression) { + codeEditorArea.replaceText(expression); + } + + + public Val textProperty() { + return Val.wrap(codeEditorArea.textProperty()); } @@ -247,26 +226,4 @@ public class SourceEditorController implements Initializable, SettingsOwner { codeEditorArea.clearStyleLayers(); } - - @Override - public List getSettings() { - List settings = new ArrayList<>(); - settings.add(new AppSetting("langVersion", () -> getLanguageVersion().getTerseName(), - this::restoreLanguageVersion)); - - settings.add(new AppSetting("code", () -> codeEditorArea.getText(), - e -> codeEditorArea.replaceText(e))); - - return settings; - } - - - private void restoreLanguageVersion(String name) { - LanguageVersion version = LanguageRegistry.findLanguageVersionByTerseName(name); - if (version != null) { - astManager.languageVersionProperty().setValue(version); - } - } - - } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java index f1beafec32..5c7a275ce0 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java @@ -4,41 +4,54 @@ package net.sourceforge.pmd.util.fxdesigner; +import java.io.IOException; import java.net.URL; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.ResourceBundle; import org.apache.commons.lang3.StringUtils; +import org.reactfx.EventStreams; +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.util.DesignerUtil; +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.CustomCodeArea; import net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.XPathSyntaxHighlighter; -import net.sourceforge.pmd.util.fxdesigner.util.settings.AppSetting; -import net.sourceforge.pmd.util.fxdesigner.util.settings.SettingsOwner; +import net.sourceforge.pmd.util.fxdesigner.util.controls.PropertyTableView; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; +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.ChoiceBox; import javafx.scene.control.ListView; import javafx.scene.control.TitledPane; -import javafx.util.StringConverter; +import javafx.stage.Modality; +import javafx.stage.Stage; /** * XPath panel controller. * * @author Clément Fournier + * @see ExportXPathWizardController * @since 6.0.0 */ public class XPathPanelController implements Initializable, SettingsOwner { @@ -48,19 +61,26 @@ public class XPathPanelController implements Initializable, SettingsOwner { private final XPathEvaluator xpathEvaluator = new XPathEvaluator(); + private final ObservableXPathRuleBuilder ruleBuilder = new ObservableXPathRuleBuilder(); + + + @FXML + private PropertyTableView propertyView; @FXML private CustomCodeArea xpathExpressionArea; @FXML private TitledPane violationsTitledPane; @FXML private ListView xpathResultListView; - + // Actually a child of the main view toolbar, but this controller is responsible for it private ChoiceBox xpathVersionChoiceBox; - XPathPanelController(DesignerRoot owner, MainDesignerController mainController) { + public XPathPanelController(DesignerRoot owner, MainDesignerController mainController) { this.designerRoot = owner; parent = mainController; + + getRuleBuilder().setClazz(XPathRule.class); } @@ -68,12 +88,24 @@ public class XPathPanelController implements Initializable, SettingsOwner { public void initialize(URL location, ResourceBundle resources) { xpathExpressionArea.setSyntaxHighlightingEnabled(new XPathSyntaxHighlighter()); - xpathResultListView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null) { - parent.onNodeItemSelected(newVal); - } - }); + EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty()) + .filter(Objects::nonNull) + .subscribe(parent::onNodeItemSelected); + Platform.runLater(this::bindToParent); + } + + + // 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(), + propertyView.rulePropertiesProperty(), propertyView::setRuleProperties); } @@ -85,20 +117,8 @@ public class XPathPanelController implements Initializable, SettingsOwner { versionItems.add(XPathRuleQuery.XPATH_1_0_COMPATIBILITY); versionItems.add(XPathRuleQuery.XPATH_2_0); - xpathVersionChoiceBox.getSelectionModel().select(xpathEvaluator.xpathVersionProperty().get()); - - choiceBox.setConverter(new StringConverter() { - @Override - public String toString(String object) { - return "XPath " + object; - } - - - @Override - public String fromString(String string) { - return string.substring(6); - } - }); + xpathVersionChoiceBox.getSelectionModel().select(XPathRuleQuery.XPATH_2_0); + choiceBox.setConverter(DesignerUtil.stringConverter(s -> "XPath " + s, s -> s.substring(6))); } @@ -111,7 +131,7 @@ public class XPathPanelController implements Initializable, SettingsOwner { public void evaluateXPath(Node compilationUnit, LanguageVersion version) { try { - String xpath = xpathExpressionArea.getText(); + String xpath = getXpathExpression(); if (StringUtils.isBlank(xpath)) { xpathResultListView.getItems().clear(); @@ -119,7 +139,11 @@ public class XPathPanelController implements Initializable, SettingsOwner { } ObservableList results - = FXCollections.observableArrayList(xpathEvaluator.evaluateQuery(compilationUnit, version, xpath)); + = FXCollections.observableArrayList(xpathEvaluator.evaluateQuery(compilationUnit, + version, + getXpathVersion(), + xpath, + ruleBuilder.getRuleProperties())); xpathResultListView.setItems(results); violationsTitledPane.setText("Matched nodes\t(" + results.size() + ")"); } catch (XPathEvaluationException e) { @@ -138,32 +162,71 @@ public class XPathPanelController implements Initializable, SettingsOwner { } + public void showExportXPathToRuleWizard() throws IOException { + // doesn't work for some reason + 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(); + } + + public void shutdown() { xpathExpressionArea.disableSyntaxHighlighting(); } - public StringProperty xpathVersionProperty() { - return xpathEvaluator.xpathVersionProperty(); + @PersistentProperty + public String getXpathExpression() { + return xpathExpressionArea.getText(); + } + + + public void setXpathExpression(String expression) { + xpathExpressionArea.replaceText(expression); + } + + + public Val xpathExpressionProperty() { + return Val.wrap(xpathExpressionArea.textProperty()); + } + + + @PersistentProperty + public String getXpathVersion() { + return getRuleBuilder().getXpathVersion(); + } + + + public void setXpathVersion(String xpathVersion) { + getRuleBuilder().setXpathVersion(xpathVersion); + } + + + public Var xpathVersionProperty() { + return getRuleBuilder().xpathVersionProperty(); + } + + + private ObservableXPathRuleBuilder getRuleBuilder() { + return ruleBuilder; } @Override - public List getSettings() { - List settings = new ArrayList<>(); - settings.add(new AppSetting("xpathVersion", () -> xpathEvaluator.xpathVersionProperty().getValue(), - v -> { - if (!"".equals(v)) { - xpathEvaluator.xpathVersionProperty().setValue(v); - } - })); - settings.add(new AppSetting("xpathCode", () -> xpathExpressionArea.getText(), (v) -> xpathExpressionArea.replaceText(v))); - - return settings; - } - - - public ObservableValue xpathExpressionProperty() { - return xpathExpressionArea.textProperty(); + public List getChildrenSettingsNodes() { + return Collections.singletonList(getRuleBuilder()); } } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ASTManager.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ASTManager.java index 0ede04da5f..39006ecc92 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ASTManager.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ASTManager.java @@ -7,6 +7,8 @@ package net.sourceforge.pmd.util.fxdesigner.model; import java.io.StringReader; import org.apache.commons.lang3.StringUtils; +import org.reactfx.value.Val; +import org.reactfx.value.Var; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; @@ -16,9 +18,6 @@ import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.util.fxdesigner.DesignerRoot; import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; - /** * Main class of the model. Manages a compilation unit. @@ -41,15 +40,11 @@ public class ASTManager { /** * Latest computed compilation unit (only null before the first call to {@link #updateCompilationUnit(String)}) */ - private ObjectProperty compilationUnit = new SimpleObjectProperty<>(); + private Var compilationUnit = Var.newSimpleVar(null); /** * Selected language version. */ - private ObjectProperty languageVersion = new SimpleObjectProperty<>(); - - { - languageVersion.setValue(LanguageRegistry.findLanguageVersionByTerseName("java 8")); - } + private Var languageVersion = Var.newSimpleVar(LanguageRegistry.getDefaultLanguage().getDefaultVersion()); public ASTManager(DesignerRoot owner) { @@ -58,21 +53,26 @@ public class ASTManager { public LanguageVersion getLanguageVersion() { - return languageVersion.get(); + return languageVersion.getValue(); } - public ObjectProperty languageVersionProperty() { + public void setLanguageVersion(LanguageVersion version) { + languageVersion.setValue(version); + } + + + public Var languageVersionProperty() { return languageVersion; } - public Node updateCompilationUnit() { - return compilationUnit.get(); + public Node getCompilationUnit() { + return compilationUnit.getValue(); } - public ObjectProperty compilationUnitProperty() { + public Val compilationUnitProperty() { return compilationUnit; } @@ -81,14 +81,16 @@ public class ASTManager { * Refreshes the compilation unit given the current parameters of the model. * * @param source Source code + * * @throws ParseAbortedException if parsing fails and cannot recover */ public Node updateCompilationUnit(String source) throws ParseAbortedException { - if (compilationUnit.get() != null - && languageVersion.get().equals(lastLanguageVersion) && StringUtils.equals(source, lastValidSource)) { - return compilationUnit.get(); + if (compilationUnit.isPresent() + && getLanguageVersion().equals(lastLanguageVersion) + && StringUtils.equals(source, lastValidSource)) { + return getCompilationUnit(); } - LanguageVersionHandler languageVersionHandler = languageVersion.get().getLanguageVersionHandler(); + LanguageVersionHandler languageVersionHandler = getLanguageVersion().getLanguageVersionHandler(); Parser parser = languageVersionHandler.getParser(languageVersionHandler.getDefaultParserOptions()); Node node; @@ -111,8 +113,8 @@ public class ASTManager { compilationUnit.setValue(node); lastValidSource = source; - lastLanguageVersion = languageVersion.get(); - return compilationUnit.get(); + lastLanguageVersion = getLanguageVersion(); + return getCompilationUnit(); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableRuleBuilder.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableRuleBuilder.java new file mode 100644 index 0000000000..71eb8ed60b --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableRuleBuilder.java @@ -0,0 +1,358 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.model; + +import java.util.Optional; + +import org.reactfx.collection.LiveArrayList; +import org.reactfx.value.Var; + +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.RulePriority; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.rules.RuleBuilder; +import net.sourceforge.pmd.util.fxdesigner.util.PropertyDescriptorSpec; +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.beans.SettingsPersistenceUtil.PersistentSequence; + +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + + +/** + * Holds info about a rule, and can build it to validate it. + * + * @author Clément Fournier + * @since 6.0.0 + */ +public class ObservableRuleBuilder implements SettingsOwner { + + private Var language = Var.newSimpleVar(LanguageRegistry.getDefaultLanguage()); + private Var name = Var.newSimpleVar(""); + private Var> clazz = Var.newSimpleVar(null); + + // doesn't contain the "xpath" and "version" properties for XPath rules + private ListProperty ruleProperties = new SimpleListProperty<>(FXCollections.observableArrayList(PropertyDescriptorSpec.extractor())); + private Var> examples = Var.newSimpleVar(new LiveArrayList<>()); + + private Var minimumVersion = Var.newSimpleVar(null); + private Var maximumVersion = Var.newSimpleVar(null); + + private Var since = Var.newSimpleVar(""); + + private Var message = Var.newSimpleVar(""); + private Var externalInfoUrl = Var.newSimpleVar(""); + private Var description = Var.newSimpleVar(""); + + private Var priority = Var.newSimpleVar(RulePriority.MEDIUM); + private Var deprecated = Var.newSimpleVar(false); + private Var usesDfa = Var.newSimpleVar(false); + private Var usesMultifile = Var.newSimpleVar(false); + private Var usesTypeResolution = Var.newSimpleVar(false); + + + public Language getLanguage() { + return language.getValue(); + } + + + public void setLanguage(Language language) { + this.language.setValue(language); + } + + + public Var languageProperty() { + return language; + } + + + @PersistentProperty + public String getName() { + return name.getValue(); + } + + + public void setName(String name) { + this.name.setValue(name); + } + + + public Var nameProperty() { + return name; + } + + + @PersistentProperty + public Class getClazz() { + return clazz.getValue(); + } + + + public void setClazz(Class clazz) { + this.clazz.setValue(clazz); + } + + + public Var> clazzProperty() { + return clazz; + } + + + @PersistentSequence + public ObservableList getRuleProperties() { + return ruleProperties.getValue(); + } + + + public void setRuleProperties(ObservableList ruleProperties) { + this.ruleProperties.setValue(ruleProperties); + } + + + public ListProperty rulePropertiesProperty() { + return ruleProperties; + } + + + public LanguageVersion getMinimumVersion() { + return minimumVersion.getValue(); + } + + + public void setMinimumVersion(LanguageVersion minimumVersion) { + this.minimumVersion.setValue(minimumVersion); + } + + + public Var minimumVersionProperty() { + return minimumVersion; + } + + + public LanguageVersion getMaximumVersion() { + return maximumVersion.getValue(); + } + + + public void setMaximumVersion(LanguageVersion maximumVersion) { + this.maximumVersion.setValue(maximumVersion); + } + + + public Var maximumVersionProperty() { + return maximumVersion; + } + + + @PersistentProperty + public String getSince() { + return since.getValue(); + } + + + public void setSince(String since) { + this.since.setValue(since); + } + + + public Var sinceProperty() { + return since; + } + + + @PersistentProperty + public String getMessage() { + return message.getValue(); + } + + + public void setMessage(String message) { + this.message.setValue(message); + } + + + public Var messageProperty() { + return message; + } + + + @PersistentProperty + public String getExternalInfoUrl() { + return externalInfoUrl.getValue(); + } + + + public void setExternalInfoUrl(String externalInfoUrl) { + this.externalInfoUrl.setValue(externalInfoUrl); + } + + + public Var externalInfoUrlProperty() { + return externalInfoUrl; + } + + + @PersistentProperty + public String getDescription() { + return description.getValue(); + } + + + public void setDescription(String description) { + this.description.setValue(description); + } + + + public Var descriptionProperty() { + return description; + } + + + public Var> getExamples() { + return examples; + } + + + public void setExamples(ObservableList examples) { + this.examples.setValue(examples); + } + + + @PersistentProperty + public RulePriority getPriority() { + return priority.getValue(); + } + + + public void setPriority(RulePriority priority) { + this.priority.setValue(priority); + } + + + public Var priorityProperty() { + return priority; + } + + + public boolean isDeprecated() { + return deprecated.getValue(); + } + + + public void setDeprecated(boolean deprecated) { + this.deprecated.setValue(deprecated); + } + + + public Var deprecatedProperty() { + return deprecated; + } + + + public boolean isUsesDfa() { + return usesDfa.getValue(); + } + + + public void setUsesDfa(boolean usesDfa) { + this.usesDfa.setValue(usesDfa); + } + + + public Var usesDfaProperty() { + return usesDfa; + } + + + public boolean isUsesMultifile() { + return usesMultifile.getValue(); + } + + + public void setUsesMultifile(boolean usesMultifile) { + this.usesMultifile.setValue(usesMultifile); + } + + + public Var usesMultifileProperty() { + return usesMultifile; + } + + + public boolean getUsesTypeResolution() { + return usesTypeResolution.getValue(); + } + + + public void setUsesTypeResolution(boolean usesTypeResolution) { + this.usesTypeResolution.setValue(usesTypeResolution); + } + + + public Var usesTypeResolutionProperty() { + return usesTypeResolution; + } + + + /** + * Returns true if the parameters of the rule are consistent and the rule can be built. + * + * @return whether the rule can be built + */ + public boolean canBuild() { + try { + build(); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + + /** + * Builds the rule. + * + * @return the built rule. + * + * @throws IllegalArgumentException if parameters are incorrect + */ + public Optional build() throws IllegalArgumentException { + + try { + RuleBuilder builder = new RuleBuilder(name.getValue(), + clazz.getValue().getCanonicalName(), + language.getValue().getTerseName()); + + builder.minimumLanguageVersion(minimumVersion.getValue().getTerseName()); + builder.maximumLanguageVersion(maximumVersion.getValue().getTerseName()); + + builder.message(message.getValue()); + builder.since(since.getValue()); + builder.externalInfoUrl(externalInfoUrl.getValue()); + builder.description(description.getValue()); + builder.priority(priority.getValue().getPriority()); + + builder.setDeprecated(deprecated.getValue()); + builder.usesDFA(usesDfa.getValue()); + builder.usesTyperesolution(usesTypeResolution.getValue()); + builder.usesMultifile(usesMultifile.getValue()); + + ruleProperties.getValue().stream().map(PropertyDescriptorSpec::build).forEach(builder::defineProperty); + examples.getValue().forEach(builder::addExample); + + return Optional.of(builder.build()); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + e.printStackTrace(); + return Optional.empty(); + } + + } + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableXPathRuleBuilder.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableXPathRuleBuilder.java new file mode 100644 index 0000000000..a3a694d724 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/ObservableXPathRuleBuilder.java @@ -0,0 +1,57 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.model; + +import java.util.Optional; + +import org.reactfx.value.Var; + +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; + + +/** + * Specialises rule builders for XPath rules. + * + * @author Clément Fournier + * @since 6.0.0 + */ +public class ObservableXPathRuleBuilder extends ObservableRuleBuilder { + + + private final Var xpathVersion = Var.newSimpleVar(DesignerUtil.defaultXPathVersion()); + private final Var xpathExpression = Var.newSimpleVar(""); + + + public String getXpathVersion() { + return xpathVersion.getValue(); + } + + + public void setXpathVersion(String xpathVersion) { + this.xpathVersion.setValue(xpathVersion); + } + + + public Var xpathVersionProperty() { + return xpathVersion; + } + + + public String getXpathExpression() { + return xpathExpression.getValue(); + } + + + public Var xpathExpressionProperty() { + return xpathExpression; + } + + + @Override + public Optional build() throws IllegalArgumentException { + return super.build(); //TODO + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/XPathEvaluator.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/XPathEvaluator.java index 6b02576313..37e838c4cc 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/XPathEvaluator.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/model/XPathEvaluator.java @@ -17,10 +17,8 @@ import net.sourceforge.pmd.RuleSets; 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.util.PropertyDescriptorSpec; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; /** * Evaluates XPath expressions. @@ -31,31 +29,22 @@ import javafx.beans.property.StringProperty; public class XPathEvaluator { - private final StringProperty xpathVersion = new SimpleStringProperty(XPathRuleQuery.XPATH_2_0); - - - public String getXpathVersion() { - return xpathVersion.get(); - } - - - public StringProperty xpathVersionProperty() { - return xpathVersion; - } - - /** * Evaluates an XPath query on the compilation unit. * * @param compilationUnit AST root * @param languageVersion language version - * @param xpathQuery query + * @param xpathVersion XPath version + * @param xpathQuery XPath query + * @param properties Properties of the rule * * @throws XPathEvaluationException if there was an error during the evaluation. The cause is preserved */ public List evaluateQuery(Node compilationUnit, LanguageVersion languageVersion, - String xpathQuery) throws XPathEvaluationException { + String xpathVersion, + String xpathQuery, + List properties) throws XPathEvaluationException { if (StringUtils.isBlank(xpathQuery)) { return Collections.emptyList(); @@ -71,11 +60,15 @@ public class XPathEvaluator { } }; + xpathRule.setMessage(""); xpathRule.setLanguage(languageVersion.getLanguage()); xpathRule.setXPath(xpathQuery); - xpathRule.setVersion(xpathVersion.get()); + xpathRule.setVersion(xpathVersion); + properties.stream() + .map(PropertyDescriptorSpec::build) + .forEach(xpathRule::definePropertyDescriptor); final RuleSet ruleSet = new RuleSetFactory().createSingleRuleRuleSet(xpathRule); @@ -94,6 +87,4 @@ public class XPathEvaluator { throw new XPathEvaluationException(e); } } - - } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/DesignerUtil.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/DesignerUtil.java index 3f2af1dd00..ed48f91ef0 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/DesignerUtil.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/DesignerUtil.java @@ -5,20 +5,27 @@ package net.sourceforge.pmd.util.fxdesigner.util; import java.io.File; +import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; -import net.sourceforge.pmd.lang.LanguageVersionHandler; import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.rule.xpath.XPathRuleQuery; + +import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; +import javafx.util.StringConverter; /** @@ -29,10 +36,10 @@ public class DesignerUtil { private static final Path PMD_SETTINGS_DIR = Paths.get(System.getProperty("user.home"), ".pmd"); - private static final File DESIGNER_SETTINGS_FILE = PMD_SETTINGS_DIR.resolve("pmd_new_designer.xml").toFile(); + private static final File DESIGNER_SETTINGS_FILE = PMD_SETTINGS_DIR.resolve("designer.xml").toFile(); - private static LanguageVersion[] supportedLanguageVersions; + private static List supportedLanguageVersions; private static Map extensionsToLanguage; @@ -41,28 +48,25 @@ public class DesignerUtil { } - private static Map getExtensionsToLanguageMap() { - Map result = new HashMap<>(); - Arrays.stream(getSupportedLanguageVersions()) - .map(LanguageVersion::getLanguage) - .distinct() - .collect(Collectors.toMap(Language::getExtensions, Language::getDefaultVersion)) - .forEach((key, value) -> key.forEach(ext -> result.put(ext, value))); - return result; + public static String defaultXPathVersion() { + return XPathRuleQuery.XPATH_2_0; } - public static LanguageVersion getLanguageVersionFromExtension(String filename) { - if (extensionsToLanguage == null) { - extensionsToLanguage = getExtensionsToLanguageMap(); - } + public static LanguageVersion defaultLanguageVersion() { + return LanguageRegistry.getDefaultLanguage().getDefaultVersion(); + } - if (filename.indexOf('.') > 0) { - String[] tokens = filename.split("\\."); - return extensionsToLanguage.get(tokens[tokens.length - 1]); - } - return null; + /** + * Gets the URL to an fxml file from its simple name. + * + * @param simpleName Simple name of the file, i.e. with no directory prefixes + * + * @return A URL to an fxml file + */ + public static URL getFxml(String simpleName) { + return DesignerUtil.class.getResource("/net/sourceforge/pmd/util/fxdesigner/fxml/" + simpleName); } @@ -76,20 +80,86 @@ public class DesignerUtil { } - public static LanguageVersion[] getSupportedLanguageVersions() { + public static StringConverter stringConverter(Function toString, Function fromString) { + return new StringConverter() { + @Override + public String toString(T object) { + return toString.apply(object); + } + + + @Override + public T fromString(String string) { + return fromString.apply(string); + } + }; + } + + + public static StringConverter languageVersionStringConverter() { + return DesignerUtil.stringConverter(LanguageVersion::getShortName, + s -> LanguageRegistry.findLanguageVersionByTerseName(s.toLowerCase())); + } + + + private static Map getExtensionsToLanguageMap() { + Map result = new HashMap<>(); + getSupportedLanguageVersions().stream() + .map(LanguageVersion::getLanguage) + .distinct() + .collect(Collectors.toMap(Language::getExtensions, + Language::getDefaultVersion)) + .forEach((key, value) -> key.forEach(ext -> result.put(ext, value))); + return result; + } + + + public static LanguageVersion getLanguageVersionFromExtension(String filename) { + if (extensionsToLanguage == null) { + extensionsToLanguage = getExtensionsToLanguageMap(); + } + + if (filename.indexOf('.') > 0) { + String[] tokens = filename.split("\\."); + return extensionsToLanguage.get(tokens[tokens.length - 1]); + } + return null; + } + + + public static List getSupportedLanguageVersions() { if (supportedLanguageVersions == null) { List languageVersions = new ArrayList<>(); for (LanguageVersion languageVersion : LanguageRegistry.findAllVersions()) { - LanguageVersionHandler languageVersionHandler = languageVersion.getLanguageVersionHandler(); - if (languageVersionHandler != null) { - Parser parser = languageVersionHandler.getParser(languageVersionHandler.getDefaultParserOptions()); - if (parser != null && parser.canParse()) { - languageVersions.add(languageVersion); - } - } + Optional.ofNullable(languageVersion.getLanguageVersionHandler()) + .map(handler -> handler.getParser(handler.getDefaultParserOptions())) + .filter(Parser::canParse) + .ifPresent(p -> languageVersions.add(languageVersion)); } - supportedLanguageVersions = languageVersions.toArray(new LanguageVersion[languageVersions.size()]); + supportedLanguageVersions = languageVersions; } return supportedLanguageVersions; } + + + /** + * Binds the underlying property to a source of values. The source property is also initialised using the setter. + * + * @param underlying The underlying property + * @param ui The property exposed to the user (the one in this wizard) + * @param setter Setter to initialise the UI value + * @param Type of values + */ + public static void rewire(Property underlying, ObservableValue ui, Consumer setter) { + setter.accept(underlying.getValue()); + underlying.unbind(); + underlying.bind(ui); // Bindings are garbage collected after the popup dies + } + + /** Like rewire, with no initialisation. */ + public static void rewire(Property underlying, ObservableValue source) { + underlying.unbind(); + underlying.bind(source); + } + } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/PropertyDescriptorSpec.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/PropertyDescriptorSpec.java new file mode 100644 index 0000000000..ae7cc0e4c7 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/PropertyDescriptorSpec.java @@ -0,0 +1,201 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util; + + +import java.util.HashMap; +import java.util.Map; + +import org.reactfx.value.Val; +import org.reactfx.value.Var; + +import net.sourceforge.pmd.properties.PropertyDescriptor; +import net.sourceforge.pmd.properties.PropertyDescriptorField; +import net.sourceforge.pmd.properties.PropertyTypeId; +import net.sourceforge.pmd.properties.builders.PropertyDescriptorExternalBuilder; +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner; +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; + +import javafx.beans.Observable; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.util.Callback; + + +/** + * Stores enough data to build a property descriptor, can be displayed within table views. + * + * @author Clément Fournier + * @since 6.0.0 + */ +public class PropertyDescriptorSpec implements SettingsOwner { + + private static final String DEFAULT_STRING = "TODO"; + + private final Val isNumerical; + private final Val isPackaged; + private final Val isMultivalue; + + private final Var typeId = Var.newSimpleVar(PropertyTypeId.STRING); + private final Var name = Var.newSimpleVar(DEFAULT_STRING); + private final Var value = Var.newSimpleVar(DEFAULT_STRING); + private final Var description = Var.newSimpleVar(DEFAULT_STRING); + + + public PropertyDescriptorSpec() { + isNumerical = typeId.map(PropertyTypeId::isPropertyNumeric); + isPackaged = typeId.map(PropertyTypeId::isPropertyPackaged); + isMultivalue = typeId.map(PropertyTypeId::isPropertyMultivalue); + } + + + public Boolean getIsNumerical() { + return isNumerical.getValue(); + } + + + public Val isNumericalProperty() { + return isNumerical; + } + + + public Boolean getIsPackaged() { + return isPackaged.getValue(); + } + + + public Val isPackagedProperty() { + return isPackaged; + } + + + public Boolean getIsMultivalue() { + return isMultivalue.getValue(); + } + + + public Val isMultivalueProperty() { + return isMultivalue; + } + + + @PersistentProperty + public String getDescription() { + return description.getValue(); + } + + + public void setDescription(String description) { + this.description.setValue(description); + } + + + public Var descriptionProperty() { + return description; + } + + + @PersistentProperty + public PropertyTypeId getTypeId() { + return typeId.getValue(); + } + + + public void setTypeId(PropertyTypeId typeId) { + this.typeId.setValue(typeId); + } + + + public Var typeIdProperty() { + return typeId; + } + + + @PersistentProperty + public String getName() { + return name.getValue(); + } + + + public void setName(String name) { + this.name.setValue(name); + } + + + public Var nameProperty() { + return name; + } + + + @PersistentProperty + public String getValue() { + return value.getValue(); + } + + + public void setValue(String value) { + this.value.setValue(value); + } + + + public Var valueProperty() { + return value; + } + + + /** + * Returns an xml string of this property definition. + * + * @return An xml string + */ + public String toXml() { + return String.format("", + getName(), getTypeId().getStringId(), getValue()); + } + + + @Override + public String toString() { + return toXml(); + } + + + /** + * Builds the descriptor. May throw IllegalArgumentException. + * + * @return the descriptor if it can be built + */ + public PropertyDescriptor build() { + PropertyDescriptorExternalBuilder externalBuilder = getTypeId().getFactory(); + Map values = new HashMap<>(); + values.put(PropertyDescriptorField.NAME, getName()); + values.put(PropertyDescriptorField.DEFAULT_VALUE, getValue()); + values.put(PropertyDescriptorField.DESCRIPTION, getDescription()); + + return externalBuilder.build(values); + } + + + /** + * Removes bindings from this property spec. + */ + public void unbind() { + typeIdProperty().unbind(); + nameProperty().unbind(); + descriptionProperty().unbind(); + valueProperty().unbind(); + } + + + /** Extractor for observable lists. */ + public static Callback extractor() { + return spec -> new Observable[]{spec.nameProperty(), spec.typeIdProperty(), spec.valueProperty()}; + } + + + public static ObservableList observableList() { + return FXCollections.observableArrayList(extractor()); + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNode.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNode.java new file mode 100644 index 0000000000..679dc8bb34 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNode.java @@ -0,0 +1,37 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.util.Collections; +import java.util.List; + + +/** + * Represents a node in the settings model. The settings model is a + * tree of such nodes, mirroring the state hierarchy of the application. + * + *

Each node can be serialised to XML. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public abstract class BeanModelNode { + + /** Makes the children accept the visitor. */ + public void childrenAccept(BeanNodeVisitor visitor, T data) { + for (BeanModelNode child : getChildrenNodes()) { + child.accept(visitor, data); + } + } + + + /** Accepts a visitor. */ + protected abstract void accept(BeanNodeVisitor visitor, T data); + + + public List getChildrenNodes() { + return Collections.emptyList(); + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNodeSeq.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNodeSeq.java new file mode 100644 index 0000000000..c079b900d2 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanModelNodeSeq.java @@ -0,0 +1,76 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentSequence; + + +/** + * Represents an indexed list of nodes sharing the same type. + * This type of node is flagged with a {@link PersistentSequence}, + * which is applied to a getter of a collection. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public class BeanModelNodeSeq extends BeanModelNode { + + private final String propertyName; + private final List children = new ArrayList<>(); + + + public BeanModelNodeSeq(String name) { + this.propertyName = name; + } + + + public void addChild(T node) { + children.add(node); + } + + + /** Returns the elements of the sequence. */ + @Override + public List getChildrenNodes() { + return children; + } + + + /** Returns the name of the property that contains the collection. */ + public String getPropertyName() { + return propertyName; + } + + + protected void accept(BeanNodeVisitor visitor, U data) { + visitor.visit(this, data); + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BeanModelNodeSeq that = (BeanModelNodeSeq) o; + return Objects.equals(propertyName, that.propertyName) + && Objects.equals(children, that.children); + } + + + @Override + public int hashCode() { + + return Objects.hash(propertyName, children); + } + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanNodeVisitor.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanNodeVisitor.java new file mode 100644 index 0000000000..82417391e5 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/BeanNodeVisitor.java @@ -0,0 +1,31 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +/** + * Implements a visitor pattern over bean nodes. Used to restore properties + * from a model and build an XML document to represent the model. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public abstract class BeanNodeVisitor { + + public void visit(BeanModelNode node, T data) { + node.childrenAccept(this, data); + } + + + public void visit(BeanModelNodeSeq node, T data) { + visit((BeanModelNode) node, data); + } + + + public void visit(SimpleBeanModelNode node, T data) { + visit((BeanModelNode) node, data); + } + + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/RestorePropertyVisitor.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/RestorePropertyVisitor.java new file mode 100644 index 0000000000..a06b19d222 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/RestorePropertyVisitor.java @@ -0,0 +1,120 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.apache.commons.beanutils.PropertyUtils; + +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; + + +/** + * Visits a bean model and restores the properties described by the nodes + * into their respective settings owner. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public class RestorePropertyVisitor extends BeanNodeVisitor { + + + @Override + public void visit(SimpleBeanModelNode model, SettingsOwner target) { + if (model == null) { + return; // possibly it wasn't saved during the previous save cycle + } + + if (target == null) { + throw new IllegalArgumentException(); + } + + if (target.getClass() != model.getNodeType()) { + throw new IllegalArgumentException("Incorrect settings restoration target, expected " + + model.getNodeType() + ", actual " + target.getClass()); + } + + Map descriptors = Arrays.stream(PropertyUtils.getPropertyDescriptors(target)) + .filter(d -> d.getReadMethod().isAnnotationPresent(PersistentProperty.class)) + .collect(Collectors.toMap(PropertyDescriptor::getName, d -> d)); + + for (Entry saved : model.getSettingsValues().entrySet()) { + if (descriptors.containsKey(saved.getKey())) { + try { + PropertyUtils.setProperty(target, saved.getKey(), saved.getValue()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + for (BeanModelNodeSeq seq : model.getSequenceProperties()) { + this.visit(seq, target); + } + + for (SettingsOwner child : target.getChildrenSettingsNodes()) { + model.getChildrenByType().get(child.getClass()).accept(this, child); + } + } + + + @Override + public void visit(BeanModelNodeSeq model, SettingsOwner target) { + if (model == null) { + return; // possibly it wasn't saved during the previous save cycle + } + + if (target == null) { + throw new IllegalArgumentException(); + } + + Collection container; + try { + @SuppressWarnings("unchecked") + Collection tmp = (Collection) PropertyUtils.getProperty(target, model.getPropertyName()); + container = tmp; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + return; + } + + + Iterator existingItems = container.iterator(); + Class itemType = null; + for (SimpleBeanModelNode child : model.getChildrenNodes()) { + SettingsOwner item; + if (existingItems.hasNext()) { + item = existingItems.next(); + } else { + if (itemType == null) { + itemType = child.getNodeType(); + } + + try { + item = (SettingsOwner) itemType.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + continue; // try hard + } + } + + child.accept(this, item); + container.add(item); + } + + try { + PropertyUtils.setProperty(target, model.getPropertyName(), container); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsOwner.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsOwner.java new file mode 100644 index 0000000000..2eb05b141e --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsOwner.java @@ -0,0 +1,28 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.util.Collections; +import java.util.List; + + +/** + * Marker interface for settings owners. Settings owners form a + * tree-like hierarchy, which is explored recursively to build + * a model of the settings to persist, under the form of a + * {@link SimpleBeanModelNode}. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public interface SettingsOwner { + + + /** Gets the children of this node in order. */ + default List getChildrenSettingsNodes() { + return Collections.emptyList(); + } + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsPersistenceUtil.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsPersistenceUtil.java new file mode 100644 index 0000000000..a47a9cabd8 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SettingsPersistenceUtil.java @@ -0,0 +1,250 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.beans.PropertyDescriptor; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Optional; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.beanutils.ConvertUtils; +import org.apache.commons.beanutils.PropertyUtils; +import org.apache.commons.io.IOUtils; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import net.sourceforge.pmd.RulePriority; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.properties.PropertyTypeId; +import net.sourceforge.pmd.util.fxdesigner.util.beans.converters.LanguageVersionConverter; +import net.sourceforge.pmd.util.fxdesigner.util.beans.converters.PropertyTypeIdConverter; +import net.sourceforge.pmd.util.fxdesigner.util.beans.converters.RulePriorityConverter; + + +/** + * Utility methods to persist settings of the application. + * + * @author Clément Fournier + * @see SimpleBeanModelNode + * @see SettingsOwner + * @since 6.1.0 + */ +public class SettingsPersistenceUtil { + + static { + // register converters for custom types + ConvertUtils.register(new RulePriorityConverter(), RulePriority.class); + ConvertUtils.register(new PropertyTypeIdConverter(), PropertyTypeId.class); + ConvertUtils.register(new LanguageVersionConverter(), LanguageVersion.class); + } + + + private SettingsPersistenceUtil() { + } + + + /** + * Restores properties contained in the file into the given object. + * + * @param root Root of the hierarchy + * @param file Properties file + */ + public static void restoreProperties(SettingsOwner root, File file) { + Optional odoc = getDocument(file); + + odoc.flatMap(XmlFormatRevision::getSuitableReader) + .map(rev -> rev.xmlInterface) + .flatMap(xmlInterface -> odoc.flatMap(xmlInterface::parseXml)) + .ifPresent(n -> restoreSettings(root, n)); + } + + + /** + * Save properties of this object and descendants into the given file. + * + * @param root Root of the hierarchy + * @param file Properties file + */ + public static void persistProperties(SettingsOwner root, File file) throws IOException { + SimpleBeanModelNode node = SettingsPersistenceUtil.buildSettingsModel(root); + XmlFormatRevision.getLatest().xmlInterface.writeModelToXml(file, node); + } + + + /** + * Returns an XML document for the given file if it exists and can be parsed. + * + * @param file File to parse + */ + private static Optional getDocument(File file) { + InputStream stream = null; + if (file.exists()) { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + stream = new FileInputStream(file); + Document document = builder.parse(stream); + return Optional.of(document); + } catch (SAXException | ParserConfigurationException | IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(stream); + } + } + + return Optional.empty(); + } + + + /** + * Builds a settings model recursively for the given settings owner. + * The properties which have a getter tagged with {@link PersistentProperty} + * are retrieved for later serialisation. + * + * @param root The root of the settings owner hierarchy. + * + * @return The built model + */ + // test only + static SimpleBeanModelNode buildSettingsModel(SettingsOwner root) { + SimpleBeanModelNode node = new SimpleBeanModelNode(root.getClass()); + + for (PropertyDescriptor d : PropertyUtils.getPropertyDescriptors(root)) { + if (d.getReadMethod() == null) { + continue; + } + + try { + if (d.getReadMethod().isAnnotationPresent(PersistentSequence.class)) { + + Object val = d.getReadMethod().invoke(root); + if (!Collection.class.isAssignableFrom(val.getClass())) { + continue; + } + + @SuppressWarnings("unchecked") + Collection values = (Collection) val; + + BeanModelNodeSeq seq = new BeanModelNodeSeq<>(d.getName()); + + for (SettingsOwner item : values) { + seq.addChild(buildSettingsModel(item)); + } + + node.addChild(seq); + } else if (d.getReadMethod().isAnnotationPresent(PersistentProperty.class)) { + node.addProperty(d.getName(), d.getReadMethod().invoke(root), d.getPropertyType()); + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + + } + + for (SettingsOwner child : root.getChildrenSettingsNodes()) { + node.addChild(buildSettingsModel(child)); + } + + return node; + } + + + /** + * Restores the settings from the model into the target. Dual of + * {@link #buildSettingsModel(SettingsOwner)}. Traverses all the + * tree. + * + * @param target Object in which to restore the properties + * @param model The model + */ + // test only + static void restoreSettings(SettingsOwner target, BeanModelNode model) { + if (model == null) { + return; // possibly it wasn't saved during the previous save cycle + } + + if (target == null) { + throw new IllegalArgumentException(); + } + + model.accept(new RestorePropertyVisitor(), target); + } + + + /** Enumerates different formats for compatibility. */ + private enum XmlFormatRevision implements Comparable { + V1(new XmlInterfaceVersion1(1)); + + private final XmlInterface xmlInterface; + + + XmlFormatRevision(XmlInterface xmlI) { + this.xmlInterface = xmlI; + } + + + public static XmlFormatRevision getLatest() { + return Arrays.stream(values()).max(Comparator.comparingInt(x -> x.xmlInterface.getRevisionNumber())).get(); + } + + + /** + * Gets a handler capable of reading the given document. + * + * @param doc The revision number + * + * @return A handler, if it can be found + */ + public static Optional getSuitableReader(Document doc) { + return Arrays.stream(values()) + .filter(rev -> rev.xmlInterface.canParse(doc)) + .findAny(); + } + } + + + /** + * Tags the *getter* of a property as suitable for persistence. + * The property will be serialized and restored on startup, so + * it must have a setter. + * + *

Properties setters and getters must respect JavaBeans + * conventions. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface PersistentProperty { + } + + + /** + * Tags the getter of a collection for persistence. The collection + * elements must implement {@link SettingsOwner} and have a noargs + * constructor. This is a solution to the problem of serializing + * collections of items of arbitrary complexity. + * + *

When restoring such a property, we assume that the property + * already has a value, and we either update existing items with + * the properties or we instantiate new items if not enough are + * already available. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface PersistentSequence { + } + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SimpleBeanModelNode.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SimpleBeanModelNode.java new file mode 100644 index 0000000000..0b8e9b8b9c --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/SimpleBeanModelNode.java @@ -0,0 +1,145 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentSequence; + + +/** + * Represents a node in the settings owner tree, and stores the values of the properties that + * should be saved and restored. A node can have other nodes as children, in which case they are + * identified using their type at restore time. To persist the properties of multiple children with + * the same type, see {@link PersistentSequence} and {@link BeanModelNodeSeq}. + * + *

This intermediary representation decouples the XML representation from the business logic, + * allowing several parsers / serializers to coexist for different versions of the schema. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public class SimpleBeanModelNode extends BeanModelNode { + + + private final Class nodeType; + + private final Map propertyValues = new HashMap<>(); + private final Map> propertyTypes = new HashMap<>(); + private final Map, BeanModelNode> children = new HashMap<>(); + private final Set> sequenceProperties = new HashSet<>(); + + + public SimpleBeanModelNode(Class nodeType) { + this.nodeType = nodeType; + } + + + /** + * Add one more property with its value. + * + * @param propertyKey Unique name identifying the property. + * @param value Value + * @param type Type of the property + */ + public void addProperty(String propertyKey, Object value, Class type) { + propertyValues.put(propertyKey, value); + propertyTypes.put(propertyKey, type); + } + + + /** + * Add a sequence of nodes as a child of this node. + * + * @param seq Sequence of nodes + */ + public void addChild(BeanModelNodeSeq seq) { + sequenceProperties.add(seq); + } + + + /** + * Add a node to the children of this node. + * + * @param child Node + */ + public void addChild(SimpleBeanModelNode child) { + children.put(child.nodeType, child); + } + + + /** Returns a map of property names to their value. */ + public Map getSettingsValues() { + return Collections.unmodifiableMap(propertyValues); + } + + + /** Returns a map of property names to their type. */ + public Map> getSettingsTypes() { + return Collections.unmodifiableMap(propertyTypes); + } + + + /** Returns a map of children by type. */ + public Map, BeanModelNode> getChildrenByType() { + return Collections.unmodifiableMap(children); + } + + + @Override + public List getChildrenNodes() { + Set allChildren = new HashSet<>(children.values()); + allChildren.addAll(sequenceProperties); + return new ArrayList<>(allChildren); + } + + + /** Gets the sequences of nodes registered as children. */ + public Set> getSequenceProperties() { + return sequenceProperties; + } + + + /** Get the type of the settings owner represented by this node. */ + public Class getNodeType() { + return nodeType; + } + + + @Override + protected void accept(BeanNodeVisitor visitor, U data) { + visitor.visit(this, data); + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleBeanModelNode that = (SimpleBeanModelNode) o; + return Objects.equals(nodeType, that.nodeType) + && Objects.equals(propertyValues, that.propertyValues) + && Objects.equals(propertyTypes, that.propertyTypes) + && Objects.equals(children, that.children) + && Objects.equals(sequenceProperties, that.sequenceProperties); + } + + + @Override + public int hashCode() { + return Objects.hash(nodeType, propertyValues, propertyTypes, children, sequenceProperties); + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterface.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterface.java new file mode 100644 index 0000000000..4667b4386b --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterface.java @@ -0,0 +1,152 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Optional; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + + +/** + * Represents a version of the Xml format used to store settings. The + * parser and serializer must understand each other, so they're kept + * together. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public abstract class XmlInterface { + + // modifying these will break compatibility + private static final String SCHEMA_MODEL_VERSION = "revision"; + private static final String SCHEMA_DOCUMENT_ELEMENT = "designer-settings"; + + private final int revisionNumber; + + + public XmlInterface(int rev) { + this.revisionNumber = rev; + } + + + public int getRevisionNumber() { + return revisionNumber; + } + + + /** + * Parses a XML document produced by {@link #writeModelToXml(File, SimpleBeanModelNode)} + * into a settings node. + * + * @param document The document to parse + * + * @return The root of the model hierarchy, or empty if the revision is not supported + */ + public final Optional parseXml(Document document) { + if (canParse(document)) { + Element rootNodeElement = (Element) document.getDocumentElement().getChildNodes().item(1); + return Optional.ofNullable(parseSettingsOwnerNode(rootNodeElement)); + } + return Optional.empty(); + } + + + /** + * Returns true if the document can be read by this object. + * + * @param document Document to test + */ + public boolean canParse(Document document) { + int docVersion = Integer.parseInt(document.getDocumentElement().getAttribute(SCHEMA_MODEL_VERSION)); + return docVersion == getRevisionNumber(); + } + + + private Document initDocument() throws IOException { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException("Failed to create settings document builder", e); + } + Document document = documentBuilder.newDocument(); + + Element settingsElement = document.createElement(SCHEMA_DOCUMENT_ELEMENT); + settingsElement.setAttribute(SCHEMA_MODEL_VERSION, "" + getRevisionNumber()); + document.appendChild(settingsElement); + return document; + } + + + /** Saves parameters to disk. */ + private void save(Document document, File outputFile) throws IOException { + try { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + + Source source = new DOMSource(document); + outputFile.getParentFile().mkdirs(); + Result result = new StreamResult(new FileWriter(outputFile)); + transformer.transform(source, result); + } catch (TransformerException e) { + throw new IOException("Failed to save settings", e); + } + } + + + /** + * Writes the model to a file. + * + * @param output The output file + * @param model The model to serialize + * + * @throws IOException If saving the settings failed + */ + public final void writeModelToXml(File output, SimpleBeanModelNode model) throws IOException { + Document document = initDocument(); + model.accept(getDocumentMakerVisitor(), document.getDocumentElement()); + save(document, output); + } + + + /** + * Parses a settings node and its descendants recursively. + * + * @param nodeElement Element to parse + * + * @return The model described by the element + */ + protected abstract SimpleBeanModelNode parseSettingsOwnerNode(Element nodeElement); + + + /** + * Gets a visitor which populates xml elements with corresponding nodes. + * + * @return A visitor + */ + protected abstract BeanNodeVisitor getDocumentMakerVisitor(); + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterfaceVersion1.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterfaceVersion1.java new file mode 100644 index 0000000000..cad6ebe6dc --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/XmlInterfaceVersion1.java @@ -0,0 +1,160 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import org.apache.commons.beanutils.ConvertUtils; +import org.apache.commons.lang3.ClassUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + +/** + * V0, really. + * + * @author Clément Fournier + * @since 6.1.0 + */ +public class XmlInterfaceVersion1 extends XmlInterface { + + + // names used in the Xml schema + private static final String SCHEMA_NODE_ELEMENT = "node"; + private static final String SCHEMA_NODESEQ_ELEMENT = "nodeseq"; + private static final String SCHEMA_NODE_CLASS_ATTRIBUTE = "class"; + private static final String SCHEMA_PROPERTY_ELEMENT = "property"; + private static final String SCHEMA_PROPERTY_NAME = "name"; + private static final String SCHEMA_PROPERTY_TYPE = "type"; + + + public XmlInterfaceVersion1(int revisionNumber) { + super(revisionNumber); + } + + + private List getChildrenByTagName(Element element, String tagName) { + NodeList children = element.getChildNodes(); + List elts = new ArrayList<>(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i).getNodeType() == Node.ELEMENT_NODE && tagName.equals(children.item(i).getNodeName())) { + elts.add((Element) children.item(i)); + } + } + + return elts; + } + + + @Override + protected SimpleBeanModelNode parseSettingsOwnerNode(Element nodeElement) { + Class clazz; + try { + clazz = Class.forName(nodeElement.getAttribute(SCHEMA_NODE_CLASS_ATTRIBUTE)); + } catch (ClassNotFoundException e) { + return null; + } + + SimpleBeanModelNode node = new SimpleBeanModelNode(clazz); + + for (Element setting : getChildrenByTagName(nodeElement, SCHEMA_PROPERTY_ELEMENT)) { + parseSingleProperty(setting, node); + } + + for (Element child : getChildrenByTagName(nodeElement, SCHEMA_NODE_ELEMENT)) { + try { + if (node.getChildrenByType().get(Class.forName(child.getAttribute(SCHEMA_NODE_CLASS_ATTRIBUTE))) == null) { // FIXME + node.addChild(parseSettingsOwnerNode(child)); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + for (Element seq : getChildrenByTagName(nodeElement, SCHEMA_NODESEQ_ELEMENT)) { + parseNodeSeq(seq, node); + } + + return node; + } + + + private void parseSingleProperty(Element propertyElement, SimpleBeanModelNode owner) { + String typeName = propertyElement.getAttribute(SCHEMA_PROPERTY_TYPE); + String name = propertyElement.getAttribute(SCHEMA_PROPERTY_NAME); + Class type; + try { + type = ClassUtils.getClass(typeName); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return; + } + + ConvertUtils.convert(new Object()); + Object value = ConvertUtils.convert(propertyElement.getTextContent(), type); + + owner.addProperty(name, value, type); + } + + + private void parseNodeSeq(Element nodeSeq, SimpleBeanModelNode parent) { + BeanModelNodeSeq built = new BeanModelNodeSeq<>(nodeSeq.getAttribute(SCHEMA_PROPERTY_NAME)); + for (Element child : getChildrenByTagName(nodeSeq, SCHEMA_NODE_ELEMENT)) { + built.addChild(parseSettingsOwnerNode(child)); + } + parent.addChild(built); + } + + + @Override + protected BeanNodeVisitor getDocumentMakerVisitor() { + return new DocumentMakerVisitor(); + } + + + public static class DocumentMakerVisitor extends BeanNodeVisitor { + + + @Override + public void visit(SimpleBeanModelNode node, Element parent) { + Element nodeElement = parent.getOwnerDocument().createElement(SCHEMA_NODE_ELEMENT); + nodeElement.setAttribute(SCHEMA_NODE_CLASS_ATTRIBUTE, node.getNodeType().getCanonicalName()); + + for (Entry keyValue : node.getSettingsValues().entrySet()) { + // I don't think the API is intended to be used like that + // but ConvertUtils wouldn't use the convertToString methods + // defined in the converters otherwise. + // Even when a built-in converter is available, objects are + // still converted with Object::toString which fucks up the + // conversion... + String value = (String) ConvertUtils.lookup(keyValue.getValue().getClass()).convert(String.class, keyValue.getValue()); + if (value == null) { + continue; + } + + Element setting = parent.getOwnerDocument().createElement(SCHEMA_PROPERTY_ELEMENT); + setting.setAttribute(SCHEMA_PROPERTY_NAME, keyValue.getKey()); + setting.setAttribute(SCHEMA_PROPERTY_TYPE, node.getSettingsTypes().get(keyValue.getKey()).getCanonicalName()); + setting.appendChild(parent.getOwnerDocument().createCDATASection(value)); + nodeElement.appendChild(setting); + } + + parent.appendChild(nodeElement); + super.visit(node, nodeElement); + } + + + @Override + public void visit(BeanModelNodeSeq node, Element parent) { + Element nodeElement = parent.getOwnerDocument().createElement(SCHEMA_NODESEQ_ELEMENT); + nodeElement.setAttribute(SCHEMA_PROPERTY_NAME, node.getPropertyName()); + parent.appendChild(nodeElement); + super.visit(node, nodeElement); + } + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/LanguageVersionConverter.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/LanguageVersionConverter.java new file mode 100644 index 0000000000..58b584b54c --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/LanguageVersionConverter.java @@ -0,0 +1,35 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans.converters; + +import org.apache.commons.beanutils.converters.AbstractConverter; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; + + +/** + * @author Clément Fournier + * @since 6.1.0 + */ +public class LanguageVersionConverter extends AbstractConverter { + + @Override + protected String convertToString(Object value) { + return ((LanguageVersion) value).getTerseName(); + } + + + @Override + protected Object convertToType(Class aClass, Object o) { + return LanguageRegistry.findLanguageVersionByTerseName(o.toString()); + } + + + @Override + protected Class getDefaultType() { + return LanguageVersion.class; + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/PropertyTypeIdConverter.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/PropertyTypeIdConverter.java new file mode 100644 index 0000000000..0967cd816c --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/PropertyTypeIdConverter.java @@ -0,0 +1,34 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans.converters; + +import org.apache.commons.beanutils.converters.AbstractConverter; + +import net.sourceforge.pmd.properties.PropertyTypeId; + + +/** + * @author Clément Fournier + * @since 6.1.0 + */ +public class PropertyTypeIdConverter extends AbstractConverter { + + @Override + protected String convertToString(Object value) { + return ((PropertyTypeId) value).getStringId(); + } + + + @Override + protected Object convertToType(Class aClass, Object o) { + return PropertyTypeId.lookupMnemonic(o.toString()); + } + + + @Override + protected Class getDefaultType() { + return PropertyTypeId.class; + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/RulePriorityConverter.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/RulePriorityConverter.java new file mode 100644 index 0000000000..22bd3dc432 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/beans/converters/RulePriorityConverter.java @@ -0,0 +1,34 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.beans.converters; + +import org.apache.commons.beanutils.converters.AbstractConverter; + +import net.sourceforge.pmd.RulePriority; + + +/** + * @author Clément Fournier + * @since 6.1.0 + */ +public class RulePriorityConverter extends AbstractConverter { + + @Override + protected String convertToString(Object value) { + return Integer.toString(((RulePriority) value).getPriority()); + } + + + @Override + protected Object convertToType(Class aClass, Object o) { + return RulePriority.valueOf(Integer.parseInt(o.toString())); + } + + + @Override + protected Class getDefaultType() { + return RulePriority.class; + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/AvailableSyntaxHighlighters.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/AvailableSyntaxHighlighters.java index cc47be38ac..f5402a1d19 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/AvailableSyntaxHighlighters.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/AvailableSyntaxHighlighters.java @@ -13,6 +13,7 @@ import net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.Java import net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.XPathSyntaxHighlighter; import net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.XmlSyntaxHighlighter; + /** * Lists the available syntax highlighter engines by language. * @@ -40,21 +41,17 @@ public enum AvailableSyntaxHighlighters { /** - * Gets the highlighter for a language if available, otherwise returns null. + * Gets the highlighter for a language if available. * * @param language Language to look for * - * @return A highlighter if available, otherwise null + * @return A highlighter, if available */ - public static SyntaxHighlighter getComputerForLanguage(Language language) { - Optional found = Arrays.stream(AvailableSyntaxHighlighters.values()) - .filter(e -> e.language.equals(language.getTerseName())) - .findFirst(); - if (found.isPresent()) { - return found.get().engine; - } else { - return null; - } + public static Optional getHighlighterForLanguage(Language language) { + return Arrays.stream(AvailableSyntaxHighlighters.values()) + .filter(e -> e.language.equals(language.getTerseName())) + .findFirst() + .map(h -> h.engine); } } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/CustomCodeArea.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/CustomCodeArea.java index d6f4fb8638..1d69468540 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/CustomCodeArea.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/CustomCodeArea.java @@ -147,7 +147,7 @@ public class CustomCodeArea extends CodeArea { try { // refresh the highlighting. Task>> t = computeHighlightingAsync(this.getText()); - t.setOnSucceeded((e) -> { + t.setOnSucceeded(e -> { StyleLayer layer = styleContext.getLayer(SYNTAX_HIGHLIGHT_LAYER_ID); layer.reset(t.getValue()); this.paintCss(); diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/StyleLayer.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/StyleLayer.java index 3a68eb7d3a..2f8ed3a075 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/StyleLayer.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/StyleLayer.java @@ -73,7 +73,7 @@ class StyleLayer { Set cssClasses) { if (endLine > codeArea.getParagraphs().size() - || endLine == codeArea.getParagraphs().size() && endColumn > codeArea.getParagraph(endLine).length()) { + || endLine == codeArea.getParagraphs().size() && endColumn > codeArea.getParagraph(endLine - 1).length()) { throw new IllegalArgumentException("Cannot style, the region is out of bounds"); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/syntaxhighlighting/XPathSyntaxHighlighter.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/syntaxhighlighting/XPathSyntaxHighlighter.java index 92c504a98a..96c0559a3e 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/syntaxhighlighting/XPathSyntaxHighlighter.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/codearea/syntaxhighlighting/XPathSyntaxHighlighter.java @@ -6,6 +6,7 @@ package net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting; import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.BRACKET; import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.KEYWORD; +import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.NUMBER; import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.PAREN; import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.SINGLEL_COMMENT; import static net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.HighlightClasses.STRING; @@ -41,8 +42,9 @@ public class XPathSyntaxHighlighter extends SimpleRegexSyntaxHighlighter { .or(XPATH_PATH.css, "//?") .or(PAREN.css, "[()]") .or(BRACKET.css, "[\\[\\]]") + .or(NUMBER.css, "\\b\\d+\\b") .or(STRING.css, "('([^'\\\\]|\\\\.)*')|(\"([^\"\\\\]|\\\\.)*\")") - .or(SINGLEL_COMMENT.css, "\\(:.*:\\)") + .or(SINGLEL_COMMENT.css, "\\(:.*?:\\)") .create(); diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/PropertyTableView.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/PropertyTableView.java new file mode 100644 index 0000000000..79a2e4d608 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/PropertyTableView.java @@ -0,0 +1,217 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util.controls; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.Collections; +import java.util.function.Consumer; + +import org.reactfx.value.Var; + +import net.sourceforge.pmd.properties.PropertyTypeId; +import net.sourceforge.pmd.util.fxdesigner.EditPropertyDialogController; +import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; +import net.sourceforge.pmd.util.fxdesigner.util.PropertyDescriptorSpec; + +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.ChoiceBoxTableCell; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.StringConverter; + + +/** + * Controls a table view used to inspect and edit the properties of + * the rule being built. This component is made to be reused in several + * views. + *

+ * TODO: would be great to make it directly editable without compromising content validation + * + * @author Clément Fournier + * @since 6.0.0 + */ +public class PropertyTableView extends TableView { + + private TableColumn propertyNameColumn = new TableColumn<>("Name"); + private TableColumn propertyTypeColumn = new TableColumn<>("Type"); + private TableColumn propertyValueColumn = new TableColumn<>("Value"); + + private SoftReference editPropertyDialogCache; + + private Var> onEditCommit = Var.newSimpleVar(null); + + + public PropertyTableView() { + initialize(); + } + + + private void initialize() { + + this.getColumns().add(propertyNameColumn); + this.getColumns().add(propertyTypeColumn); + this.getColumns().add(propertyValueColumn); + this.setColumnResizePolicy(CONSTRAINED_RESIZE_POLICY); + this.setTableMenuButtonVisible(true); + + ObservableList availableBuilders = FXCollections.observableArrayList(PropertyTypeId.typeIdsToConstants().values()); + Collections.sort(availableBuilders); + StringConverter converter = DesignerUtil.stringConverter(PropertyTypeId::getStringId, PropertyTypeId::lookupMnemonic); + propertyTypeColumn.setCellFactory(ChoiceBoxTableCell.forTableColumn(converter, availableBuilders)); + propertyNameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); + propertyValueColumn.setCellValueFactory(new PropertyValueFactory<>("value")); + propertyTypeColumn.setCellValueFactory(new PropertyValueFactory<>("typeId")); + + this.setPlaceholder(new Label("Right-click to add properties")); + + MenuItem editItem = new MenuItem("Edit..."); + editItem.setOnAction(e -> { + PropertyDescriptorSpec spec = this.getSelectionModel().getSelectedItem(); + if (spec != null) { + popEditPropertyDialog(spec); + } + }); + + MenuItem removeItem = new MenuItem("Remove"); + removeItem.setOnAction(e -> { + PropertyDescriptorSpec selected = this.getSelectionModel().getSelectedItem(); + if (selected != null) { + this.getItems().remove(selected); + } + }); + + MenuItem addItem = new MenuItem("Add property..."); + addItem.setOnAction(e -> onAddPropertyClicked()); + + ContextMenu fullMenu = new ContextMenu(); + fullMenu.getItems().addAll(editItem, removeItem, new SeparatorMenuItem(), addItem); + + // Reduced context menu, for when there are no properties or none is selected + MenuItem addItem2 = new MenuItem("Add property..."); + addItem2.setOnAction(e -> onAddPropertyClicked()); + + ContextMenu smallMenu = new ContextMenu(); + smallMenu.getItems().add(addItem2); + + this.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { + if (t.getButton() == MouseButton.SECONDARY + || t.getButton() == MouseButton.PRIMARY && t.getClickCount() > 1) { + if (this.getSelectionModel().getSelectedItem() != null) { + fullMenu.show(this, t.getScreenX(), t.getScreenY()); + } else { + smallMenu.show(this, t.getScreenX(), t.getScreenY()); + } + } + }); + + propertyNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + propertyValueColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + this.setEditable(false); + } + + + private void onAddPropertyClicked() { + PropertyDescriptorSpec spec = new PropertyDescriptorSpec(); + this.getItems().add(spec); + popEditPropertyDialog(spec); + } + + + /** + * Pops an edition dialog for the given descriptor spec. The dialog is cached and + * reused, so that it's parsed a minimal amount of time. + * + * @param edited The edited property descriptor + */ + private void popEditPropertyDialog(PropertyDescriptorSpec edited) { + if (editPropertyDialogCache == null || editPropertyDialogCache.get() == null) { + try { + editPropertyDialogCache = new SoftReference<>(createEditPropertyDialog()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Stage dialog = editPropertyDialogCache.get(); + EditPropertyDialogController wizard = (EditPropertyDialogController) dialog.getUserData(); + Platform.runLater(() -> wizard.bindToDescriptor(edited, getRuleProperties())); + dialog.setOnHiding(e -> { + edited.unbind(); + onEditCommit.ifPresent(handler -> handler.accept(edited)); + }); + dialog.show(); + } + + + private Stage createEditPropertyDialog() throws IOException { + EditPropertyDialogController wizard = new EditPropertyDialogController(); + + FXMLLoader loader = new FXMLLoader(DesignerUtil.getFxml("edit-property-dialog.fxml")); + loader.setController(wizard); + + final Stage dialog = new Stage(); + dialog.initOwner(this.getScene().getWindow()); + dialog.initModality(Modality.WINDOW_MODAL); + dialog.initStyle(StageStyle.UNDECORATED); + + Parent root = loader.load(); + + Scene scene = new Scene(root); + dialog.setTitle("Edit property"); + dialog.setScene(scene); + dialog.setUserData(wizard); + return dialog; + } + + + public ObservableList getRuleProperties() { + return this.getItems(); + } + + + public void setRuleProperties(ObservableList ruleProperties) { + this.setItems(ruleProperties); + } + + + public ObjectProperty> rulePropertiesProperty() { + return this.itemsProperty(); + } + + + public Consumer getOnEditCommit() { + return onEditCommit.getValue(); + } + + + public Var> onEditCommitProperty() { + return onEditCommit; + } + + + public void setOnEditCommit(Consumer onEditCommit) { + this.onEditCommit.setValue(onEditCommit); + } + + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeCell.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeCell.java index d965754c79..b2a259e3cf 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeCell.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeCell.java @@ -10,10 +10,11 @@ import net.sourceforge.pmd.lang.java.symboltable.MethodNameDeclaration; import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration; import net.sourceforge.pmd.lang.symboltable.NameDeclaration; import net.sourceforge.pmd.lang.symboltable.Scope; +import net.sourceforge.pmd.util.fxdesigner.MainDesignerController; import javafx.scene.control.TreeCell; -import javafx.scene.control.TreeView; -import javafx.util.Callback; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; /** @@ -24,6 +25,13 @@ import javafx.util.Callback; */ public class ScopeHierarchyTreeCell extends TreeCell { + private final MainDesignerController controller; + + + public ScopeHierarchyTreeCell(MainDesignerController controller) { + this.controller = controller; + } + @Override protected void updateItem(Object item, boolean empty) { super.updateItem(item, empty); @@ -35,6 +43,15 @@ public class ScopeHierarchyTreeCell extends TreeCell { setText(item instanceof Scope ? getTextForScope((Scope) item) : getTextForDeclaration((NameDeclaration) item)); } + + this.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { + if (t.getButton() == MouseButton.PRIMARY + && getTreeView().getSelectionModel().getSelectedItem() == item) { + if (item instanceof NameDeclaration) { + controller.onNodeItemSelected(((NameDeclaration) item).getNode()); + } + } + }); } @@ -71,9 +88,4 @@ public class ScopeHierarchyTreeCell extends TreeCell { return sb.toString(); } - - public static Callback, ScopeHierarchyTreeCell> callback() { - return param -> new ScopeHierarchyTreeCell(); - } - } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeItem.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeItem.java index 3b26d5ced2..72faf47399 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeItem.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/ScopeHierarchyTreeItem.java @@ -32,6 +32,7 @@ public class ScopeHierarchyTreeItem extends TreeItem { * Gets the scope hierarchy of a node. * * @param node Node + * * @return Root of the tree */ public static ScopeHierarchyTreeItem buildAscendantHierarchy(Node node) { @@ -67,7 +68,9 @@ public class ScopeHierarchyTreeItem extends TreeItem { if (parent == null) { return scopeTreeNode; } else { - parent.getChildren().add(scopeTreeNode); + if (scopeTreeNode.getChildren().size() > 0) { // hides empty scopes + parent.getChildren().add(scopeTreeNode); + } return scopeTreeNode; } } @@ -79,5 +82,5 @@ public class ScopeHierarchyTreeItem extends TreeItem { } return null; } - + } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/AppSetting.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/AppSetting.java deleted file mode 100644 index 4be4c527fe..0000000000 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/AppSetting.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.util.fxdesigner.util.settings; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * @author Clément Fournier - * @since 6.0.0 - */ -public class AppSetting { - - private final String keyName; - private final Supplier getValueFunction; - private final Consumer setValueFunction; - - - public AppSetting(String keyName, Supplier getValueFunction, - Consumer setValueFunction) { - this.keyName = keyName; - this.getValueFunction = getValueFunction; - this.setValueFunction = setValueFunction; - } - - - public String getValue() { - return getValueFunction.get(); - } - - - public void setValue(String value) { - setValueFunction.accept(value); - } - - - public String getKeyName() { - return keyName; - } -} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/SettingsOwner.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/SettingsOwner.java deleted file mode 100644 index 0fd67f3379..0000000000 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/SettingsOwner.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.util.fxdesigner.util.settings; - -import java.util.List; - -/** - * @author Clément Fournier - * @since 6.0.0 - */ -public interface SettingsOwner { - - - /** - * Gets the settings of this specific object. - * - * @return The settings - */ - List getSettings(); - - -} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsLoader.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsLoader.java deleted file mode 100644 index 5af9e225aa..0000000000 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsLoader.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.util.fxdesigner.util.settings; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.apache.commons.io.IOUtils; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - - -/** - * Loads settings stored in the format of {@link XMLSettingsSaver}. - * - * @author Clément Fournier - * @since 6.0.0 - */ -public class XMLSettingsLoader { - - private final File settingsFile; - - - public XMLSettingsLoader(File settingsPath) { - this.settingsFile = settingsPath; - } - - - private Set getSettingNodes(Document document) { - NodeList nodes = document.getElementsByTagName("setting"); - Set set = new HashSet<>(); - for (int i = 0; i < nodes.getLength(); i++) { - set.add((Element) nodes.item(i)); - } - - return set; - } - - - public Map getSettings() throws IOException { - InputStream stream = null; - try { - - - if (settingsFile.exists()) { - - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - stream = new FileInputStream(settingsFile); - Document document = builder.parse(stream); - - Set settings = getSettingNodes(document); - - return settings.stream() - .collect(Collectors.toMap((elt) -> elt.getAttribute("key"), - Node::getTextContent)); - - } - } catch (SAXException | ParserConfigurationException | IOException e) { - throw new IOException("Failed to load settings", e); - } finally { - IOUtils.closeQuietly(stream); - - } - - return Collections.emptyMap(); - } -} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsSaver.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsSaver.java deleted file mode 100644 index ba068a1348..0000000000 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/settings/XMLSettingsSaver.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.util.fxdesigner.util.settings; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Result; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; - - -/** - * Saves settings to disk as key-value pairs. This implementation stores them into an XML file. - * - * @author Clément Fournier - * @since 6.0.0 - */ -public class XMLSettingsSaver { - - private final File outputFile; - private Document document; - - - private XMLSettingsSaver(File output) throws IOException { - this.outputFile = output; - - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder; - try { - documentBuilder = documentBuilderFactory.newDocumentBuilder(); - } catch (ParserConfigurationException e) { - throw new IOException("Failed to create settings document builder", e); - } - document = documentBuilder.newDocument(); - - Element settingsElement = document.createElement("settings"); - document.appendChild(settingsElement); - } - - - /** - * Saves a key value pair. - */ - public XMLSettingsSaver put(String key, String value) { - Element settingElement = document.createElement("setting"); - settingElement.setAttribute("key", key); - settingElement.appendChild(document.createCDATASection(value)); - document.getDocumentElement().appendChild(settingElement); - - return this; - } - - - /** Saves parameters to disk. */ - public void save() throws IOException { - try { - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - - transformer.setOutputProperty(OutputKeys.METHOD, "xml"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - - Source source = new DOMSource(document); - outputFile.getParentFile().mkdirs(); - Result result = new StreamResult(new FileWriter(outputFile)); - transformer.transform(source, result); - } catch (TransformerException e) { - throw new IOException("Failed to save settings", e); - - } - } - - - /** - * Get an instance. - * - * @throws IOException if initialisation fails - */ - public static XMLSettingsSaver forFile(File file) throws IOException { - return new XMLSettingsSaver(file); - } - - -} diff --git a/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/css/designer.css b/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/css/designer.css index 6a025d44c0..0bff0a645e 100644 --- a/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/css/designer.css +++ b/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/css/designer.css @@ -24,6 +24,13 @@ -fx-padding: -1 0 0 0; } +.table-view .show-hide-columns-button { + -fx-background-color: derive(-fx-base, -10%); + -fx-border-color: derive(-fx-base, -20%); + -fx-border-style: none solid solid solid; + -fx-padding: -1 0 0 0; +} + .text-area { -fx-background-insets: 0; -fx-background-color: transparent, white, transparent, white; @@ -36,12 +43,10 @@ -fx-border-width: -1; } - .text-area .content { -fx-background-color: transparent, white, transparent, white; } - /**************/ /* Split pane */ /**************/ @@ -61,11 +66,6 @@ } -.bottom-pane-split-pane.split-pane > .split-pane-divider { - /*-fx-background-color: derive(-fx-base, -20%);*/ -} - - .secondary-panel { -fx-border-style: none; } @@ -76,7 +76,6 @@ -fx-min-width: 300px; } - /************/ /* Toolbars */ /************/ @@ -91,20 +90,26 @@ -fx-pref-height: 24.0; -fx-border-radius: 0.0; -fx-background-radius: 0.0; - -fx-background-color: derive(-fx-base, -10%); + -fx-background-color: derive(-fx-base, -14%); } -.titled-pane > .title { - +.titled-pane.accent-header > .title, +.tool-bar.accent-header, +.tool-bar.info-title-bar, +.split-pane.accent-header > .split-pane-divider, +#main-toolbar, #main-vertical-split-pane > .split-pane-divider { + -fx-background-color: derive(-fx-base, -14%); } -.menu-bar { - -fx-background-color: derive(-fx-base, -10%); - -fx-border-style: none none solid none; - -fx-border-color: derive(-fx-base, -20%); +.accordion .titled-pane .tool-bar { + -fx-background-color: derive(-fx-base, -15%); } -#mainToolbar { +.accordion .titled-pane .tool-bar .button { + -fx-padding: 2 5 2 5; +} + +.tool-bar { -fx-pref-height: 30px; -fx-max-height: 30px; -fx-min-height: 30px; @@ -112,18 +117,24 @@ -fx-border-width: .6; } -.button { +.tool-bar .button { + -fx-padding: -3 5 -3 5; -fx-background-color: derive(-fx-base, 10%); -fx-border-color: derive(-fx-base, -20%); -fx-border-radius: 3; } -.choice-box { +.tool-bar .choice-box { -fx-background-color: derive(-fx-base, 10%); -fx-border-color: derive(-fx-base, -20%); -fx-border-radius: 3; } +.titled-pane.bar-sep > .title { + -fx-border-style: none none solid none; + -fx-border-width: 1; +} + .toggle-button:selected.expand-toggle { -fx-shape: "M 0 0 h 7 l -3.5 4 z"; } @@ -141,7 +152,6 @@ -fx-pref-width: 10; } - /************/ /* Tab pane */ /************/ @@ -160,12 +170,13 @@ .tab { -fx-background-insets: 0.0; -fx-background-radius: 0.0; + -fx-padding: 0 30 0 30; -fx-border-color: transparent; -fx-background-color: transparent; } .tab:selected { - -fx-background-color: derive(-fx-base, -20%); + -fx-background-color: derive(-fx-base, -23%); } .tab:focused { @@ -176,8 +187,21 @@ -fx-background-color: transparent; } -.tab { - -fx-padding: 0 30 0 30; +.tab-pane.bottom-pane-tab-pane .tab-header-area .tab:selected { + -fx-border-style: none none solid none; + -fx-border-insets: 0 0 1pt 0; + -fx-border-width: 0 0 1pt 0; + /*-fx-border-color: derive(-fx-base, -10%);*/ +} + + +.tab-pane .tab-header-area .tab-header-background, +.menu-bar { + -fx-border-style: none none solid none; + -fx-border-insets: 0 0 1pt 0; + -fx-border-width: 0 0 1pt 0; + -fx-background-color: -fx-base, derive(-fx-base, -4%); + -fx-border-color: transparent; } .tab-pane .tab-header-background { @@ -203,7 +227,6 @@ /* Scroll bars */ /***************/ - .scroll-bar * { -fx-background-color: white; } diff --git a/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/fxml/designer.fxml b/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/fxml/designer.fxml index 9865f7efd8..77ea090688 100644 --- a/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/fxml/designer.fxml +++ b/pmd-ui/src/main/resources/net/sourceforge/pmd/util/fxdesigner/fxml/designer.fxml @@ -2,14 +2,9 @@ - - - - - @@ -22,43 +17,39 @@ + - - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
- + @@ -70,7 +61,7 @@
- +