From 749ab62b0dbc8e8326f693cae75a8f232e8822a8 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 28 Jul 2026 09:52:00 +0200 Subject: [PATCH 01/30] Remove legacy project dir --- .../xdev/pmd/util/io/ProjectFilePaths.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java index 6856ded..af74f95 100644 --- a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java +++ b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java @@ -1,7 +1,5 @@ package software.xdev.pmd.util.io; -import static java.util.Arrays.asList; - import java.io.File; import java.util.function.Function; @@ -19,7 +17,6 @@ public class ProjectFilePaths private static final Logger LOG = Logger.getInstance(ProjectFilePaths.class); private static final String IDEA_PROJECT_DIR = "$PROJECT_DIR$"; - private static final String LEGACY_PROJECT_DIR = "$PRJ_DIR$"; private final Project project; private final char separatorChar; @@ -121,23 +118,20 @@ public String detokenize(@Nullable final String tokenisedPath) private String replaceProjectToken(final String path) { - for(final String projectDirToken : asList(IDEA_PROJECT_DIR, LEGACY_PROJECT_DIR)) + final int prefixLocation = path.indexOf(IDEA_PROJECT_DIR); + if(prefixLocation >= 0) { - final int prefixLocation = path.indexOf(projectDirToken); - if(prefixLocation >= 0) + final File projectPath = this.projectPath(); + if(projectPath != null) + { + final String projectRelativePath = + this.toSystemPath(path.substring(prefixLocation + IDEA_PROJECT_DIR.length())); + final String completePath = projectPath + File.separator + projectRelativePath; + return this.absolutePathOf.apply(new File(completePath)); + } + else { - final File projectPath = this.projectPath(); - if(projectPath != null) - { - final String projectRelativePath = - this.toSystemPath(path.substring(prefixLocation + projectDirToken.length())); - final String completePath = projectPath + File.separator + projectRelativePath; - return this.absolutePathOf.apply(new File(completePath)); - } - else - { - LOG.warn("Could not detokenize path as project dir is unset: " + path); - } + LOG.warn("Could not detokenize path as project dir is unset: " + path); } } return null; From a607dfcaef4320fc5d1a9bdc97b566726e6d1c6c Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 28 Jul 2026 09:53:18 +0200 Subject: [PATCH 02/30] Abstract and split up Config UI code --- .../RelativeFileConfigurationLocation.java | 8 +- .../pmd/ui/config/project/PMDConfigPanel.java | 163 ++--------------- .../FileMaskPanelManager.java} | 14 +- .../project/{ => location}/CompletePanel.java | 2 +- .../project/{ => location}/ErrorPanel.java | 2 +- .../{ => location}/LocationDialog.java | 2 +- .../project/location/LocationManager.java | 164 ++++++++++++++++++ .../project/{ => location}/LocationPanel.java | 4 +- .../{ => location}/LocationTableModel.java | 2 +- 9 files changed, 195 insertions(+), 166 deletions(-) rename src/main/java/software/xdev/pmd/ui/config/project/{FileMaskPanelContainer.java => exclusion/FileMaskPanelManager.java} (94%) rename src/main/java/software/xdev/pmd/ui/config/project/{ => location}/CompletePanel.java (92%) rename src/main/java/software/xdev/pmd/ui/config/project/{ => location}/ErrorPanel.java (97%) rename src/main/java/software/xdev/pmd/ui/config/project/{ => location}/LocationDialog.java (99%) create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java rename src/main/java/software/xdev/pmd/ui/config/project/{ => location}/LocationPanel.java (98%) rename src/main/java/software/xdev/pmd/ui/config/project/{ => location}/LocationTableModel.java (98%) diff --git a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java index 1301734..2cf4260 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java @@ -5,6 +5,7 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.util.io.ProjectFilePaths; /** @@ -34,8 +35,9 @@ public void setLocation(final String location) throw new IllegalArgumentException("A non-blank location is required"); } - super.setLocation(this.projectFilePaths().tokenise( - this.projectFilePaths().makeProjectRelative( - this.projectFilePaths().detokenize(location)))); + final ProjectFilePaths projectFilePaths = this.projectFilePaths(); + super.setLocation(projectFilePaths.tokenise( + projectFilePaths.makeProjectRelative( + projectFilePaths.detokenize(location)))); } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index 668a80a..abfa35b 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -1,8 +1,6 @@ package software.xdev.pmd.ui.config.project; import java.awt.BorderLayout; -import java.awt.Dialog; -import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -14,27 +12,15 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JTable; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.table.TableColumn; import org.jetbrains.annotations.NotNull; import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.ui.Messages; -import com.intellij.ui.AnActionButton; -import com.intellij.ui.AnActionButtonRunnable; -import com.intellij.ui.AnActionButtonUpdater; -import com.intellij.ui.TitledSeparator; -import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.panels.HorizontalLayout; -import com.intellij.ui.table.JBTable; import com.intellij.util.ui.JBUI; import software.xdev.pmd.config.PatternContainer; @@ -42,6 +28,8 @@ import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; +import software.xdev.pmd.ui.config.project.exclusion.FileMaskPanelManager; +import software.xdev.pmd.ui.config.project.location.LocationManager; /** @@ -51,11 +39,6 @@ public class PMDConfigPanel extends JPanel { private static final Insets COMPONENT_INSETS = JBUI.insets(4); - private static final int ACTIVE_COL_MIN_WIDTH = 40; - private static final int ACTIVE_COL_MAX_WIDTH = 55; - private static final int DESC_COL_MIN_WIDTH = 100; - private static final int DESC_COL_MAX_WIDTH = 200; - private static final Dimension DECORATOR_DIMENSIONS = new Dimension(300, 50); private final JLabel lblScopeDropdown = new JLabel("Scan Scope:"); private final ComboBox cbScope = new ComboBox<>(ScanScope.values()); @@ -64,10 +47,9 @@ public class PMDConfigPanel extends JPanel private final JBCheckBox chbxUseCacheFile = new JBCheckBox("Use cache file"); private final JBCheckBox chbxImportSettingsFromMaven = new JBCheckBox("Import settings from Maven"); - private final LocationTableModel locationModel = new LocationTableModel(); - private final JBTable locationTable = new JBTable(this.locationModel); + private final LocationManager locationManager = new LocationManager(); - private final FileMaskPanelContainer exclusionPanelContainer = new FileMaskPanelContainer( + private final FileMaskPanelManager exclusionPanelManager = new FileMaskPanelManager( "Exclusions", "Nothing excluded", "Add exclusion", @@ -86,7 +68,7 @@

Ignores certain files (patterns). """ ); - private final Project project; + final Project project; public PMDConfigPanel(@NotNull final Project project) { @@ -132,11 +114,11 @@ private JPanel buildConfigPanel() this.createDefaultGridBagConstraints(0, 2, 2)); configFilePanel.add( - this.buildRuleFilePanel(), + this.locationManager.buildRuleFilePanel(this.project, this), this.createFullWidthGridBagConstraints(3, 1.0)); configFilePanel.add( - this.exclusionPanelContainer.getPanel(), + this.exclusionPanelManager.getPanel(), this.createFullWidthGridBagConstraints(4, 0.1)); return configFilePanel; @@ -186,49 +168,6 @@ private GridBagConstraints createFullWidthGridBagConstraints(final int gridY, fi 0); } - private JPanel buildRuleFilePanel() - { - this.setColumnWith(this.locationTable, 0, ACTIVE_COL_MIN_WIDTH, ACTIVE_COL_MAX_WIDTH, ACTIVE_COL_MAX_WIDTH); - this.setColumnWith(this.locationTable, 1, DESC_COL_MIN_WIDTH, DESC_COL_MAX_WIDTH, DESC_COL_MAX_WIDTH); - this.locationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); - this.locationTable.setStriped(true); - this.locationTable.getTableHeader().setReorderingAllowed(false); - - final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable); - tableDecorator.setAddAction(new AddLocationAction()); - tableDecorator.setRemoveAction(new RemoveLocationAction()); - tableDecorator.setEditActionUpdater(new EnableWhenSelected()); - tableDecorator.setRemoveActionUpdater(new EnableWhenSelectedAndRemovable()); - tableDecorator.setPreferredSize(DECORATOR_DIMENSIONS); - - final JPanel container = new JPanel(new BorderLayout()); - container.add(new TitledSeparator("Configuration File"), BorderLayout.NORTH); - container.add(tableDecorator.createPanel(), BorderLayout.CENTER); - final JLabel infoLabel = new JLabel( - "The active rules file may be overridden, or deactivated, by module settings.", - AllIcons.General.Information, SwingConstants.LEFT); - infoLabel.setBorder(JBUI.Borders.empty(8, 0, 4, 0)); - container.add(infoLabel, BorderLayout.SOUTH); - return container; - } - - private void setColumnWith( - final JTable table, - final int columnIndex, - final int minSize, - final int preferredSize, - final Integer maxSize) - { - final TableColumn column = table.getColumnModel().getColumn(columnIndex); - column.setMinWidth(minSize); - column.setWidth(preferredSize); - column.setPreferredWidth(preferredSize); - if(maxSize != null) - { - column.setMaxWidth(maxSize); - } - } - public void showPluginConfiguration(@NotNull final PluginConfiguration pluginConfig) { this.cbScope.setSelectedItem(pluginConfig.scanScope()); @@ -236,9 +175,9 @@ public void showPluginConfiguration(@NotNull final PluginConfiguration pluginCon this.chbxShowSuppressedWarnings.setSelected(pluginConfig.showSuppressedWarnings()); this.chbxUseCacheFile.setSelected(pluginConfig.useCacheFile()); this.chbxImportSettingsFromMaven.setSelected(pluginConfig.importSettingsFromMaven()); - this.locationModel.setLocations(new ArrayList<>(pluginConfig.locations())); - this.locationModel.setActiveLocations(pluginConfig.getActiveLocations()); - this.exclusionPanelContainer.update(pluginConfig.projectRelativeFileExclusions().stream() + this.locationManager.locationModel().setLocations(new ArrayList<>(pluginConfig.locations())); + this.locationManager.locationModel().setActiveLocations(pluginConfig.getActiveLocations()); + this.exclusionPanelManager.update(pluginConfig.projectRelativeFileExclusions().stream() .map(PatternContainer::patternString) .collect(Collectors.toCollection(TreeSet::new))); } @@ -252,88 +191,12 @@ public PluginConfiguration getPluginConfiguration() .withScanScope(Objects.requireNonNullElseGet( (ScanScope)this.cbScope.getSelectedItem(), ScanScope::getDefaultValue)) - .withProjectRelativeFileExclusionsRaw(this.exclusionPanelContainer.getPatterns()) - .withLocations(new TreeSet<>(this.locationModel.getLocations())) - .withActiveLocationIds(this.locationModel.getActiveLocations().stream() + .withProjectRelativeFileExclusionsRaw(this.exclusionPanelManager.getPatterns()) + .withLocations(new TreeSet<>(this.locationManager.locationModel().getLocations())) + .withActiveLocationIds(this.locationManager.locationModel().getActiveLocations().stream() .map(ConfigurationLocation::getId) .collect(Collectors.toCollection(TreeSet::new))) .withImportSettingFromMaven(this.chbxImportSettingsFromMaven.isSelected()) .build(); } - - /** - * Process the addition of a configuration location. - */ - private final class AddLocationAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final LocationDialog dialogue = new LocationDialog( - PMDConfigPanel.this.parentDialogue(), - PMDConfigPanel.this.project); - - if(dialogue.showAndGet()) - { - final ConfigurationLocation newLocation = dialogue.getConfigurationLocation(); - if(PMDConfigPanel.this.locationModel.getLocations().contains(newLocation)) - { - Messages.showWarningDialog( - PMDConfigPanel.this.project, - "This location has already been added", - "Duplicate Location"); - } - else - { - PMDConfigPanel.this.locationModel.addLocation(dialogue.getConfigurationLocation()); - } - } - } - } - - private Dialog parentDialogue() - { - return (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, PMDConfigPanel.this); - } - - /** - * Process the removal of a configuration location. - */ - private final class RemoveLocationAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final int selectedIndex = PMDConfigPanel.this.locationTable.getSelectedRow(); - if(selectedIndex == -1) - { - return; - } - - PMDConfigPanel.this.locationModel.removeLocationAt(selectedIndex); - } - } - - - private final class EnableWhenSelectedAndRemovable implements AnActionButtonUpdater - { - @Override - public boolean isEnabled(@NotNull final AnActionEvent e) - { - final int selectedItem = PMDConfigPanel.this.locationTable.getSelectedRow(); - return selectedItem >= 0 && PMDConfigPanel.this.locationModel.getLocationAt(selectedItem) - .isRemovable(); - } - } - - - private final class EnableWhenSelected implements AnActionButtonUpdater - { - @Override - public boolean isEnabled(@NotNull final AnActionEvent e) - { - final int selectedItem = PMDConfigPanel.this.locationTable.getSelectedRow(); - return selectedItem >= 0; - } - } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/FileMaskPanelContainer.java b/src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java similarity index 94% rename from src/main/java/software/xdev/pmd/ui/config/project/FileMaskPanelContainer.java rename to src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java index 25f7a1e..6504569 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/FileMaskPanelContainer.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.exclusion; import java.awt.BorderLayout; import java.awt.Dimension; @@ -22,7 +22,7 @@ import software.xdev.pmd.config.PatternContainer; -class FileMaskPanelContainer +public class FileMaskPanelManager { private SortedSet patterns = new TreeSet<>(); @@ -34,7 +34,7 @@ class FileMaskPanelContainer private final JPanel patternPanel; private final JPanel panel; - FileMaskPanelContainer( + public FileMaskPanelManager( final String textTitle, final String textEmpty, final String textAddTitle, @@ -45,7 +45,7 @@ class FileMaskPanelContainer } @SuppressWarnings("checkstyle:MagicNumber") - FileMaskPanelContainer( + FileMaskPanelManager( final String textTitle, final String textEmpty, final String textAddTitle, @@ -88,19 +88,19 @@ private AnActionButtonRunnable getEditActionButtonRunnable( }; } - JPanel getPanel() + public JPanel getPanel() { return this.panel; } - void update(final SortedSet patterns) + public void update(final SortedSet patterns) { this.patterns = new TreeSet<>(patterns); this.patternModels.clear(); this.patternModels.addAllSorted(patterns); } - SortedSet getPatterns() + public SortedSet getPatterns() { return this.patterns; } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/CompletePanel.java b/src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java similarity index 92% rename from src/main/java/software/xdev/pmd/ui/config/project/CompletePanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java index 9b36a59..94121f8 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/CompletePanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.location; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/ErrorPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java similarity index 97% rename from src/main/java/software/xdev/pmd/ui/config/project/ErrorPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java index 0890342..a1c59f2 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/ErrorPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.location; import static javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS; import static javax.swing.JScrollPane.VERTICAL_SCROLLBAR_ALWAYS; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java similarity index 99% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationDialog.java rename to src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java index 66307a4..c57176a 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.location; import java.awt.BorderLayout; import java.awt.Dialog; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java new file mode 100644 index 0000000..f83400a --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java @@ -0,0 +1,164 @@ +package software.xdev.pmd.ui.config.project.location; + +import java.awt.BorderLayout; +import java.awt.Dialog; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.table.TableColumn; + +import org.jetbrains.annotations.NotNull; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.AnActionButtonRunnable; +import com.intellij.ui.AnActionButtonUpdater; +import com.intellij.ui.TitledSeparator; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.table.JBTable; +import com.intellij.util.ui.JBUI; + +import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.ui.config.project.PMDConfigPanel; + + +public class LocationManager +{ + private static final int ACTIVE_COL_MIN_WIDTH = 40; + private static final int ACTIVE_COL_MAX_WIDTH = 55; + private static final int DESC_COL_MIN_WIDTH = 100; + private static final int DESC_COL_MAX_WIDTH = 200; + private static final Dimension DECORATOR_DIMENSIONS = new Dimension(300, 50); + + final LocationTableModel locationModel = new LocationTableModel(); + final JBTable locationTable = new JBTable(this.locationModel); + + public JPanel buildRuleFilePanel(final Project project, final PMDConfigPanel pmdConfigPanel) + { + this.setColumnWith(this.locationTable, 0, ACTIVE_COL_MIN_WIDTH, ACTIVE_COL_MAX_WIDTH, ACTIVE_COL_MAX_WIDTH); + this.setColumnWith(this.locationTable, 1, DESC_COL_MIN_WIDTH, DESC_COL_MAX_WIDTH, DESC_COL_MAX_WIDTH); + this.locationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + this.locationTable.setStriped(true); + this.locationTable.getTableHeader().setReorderingAllowed(false); + + final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable); + tableDecorator.setAddAction(new AddLocationAction(project, pmdConfigPanel)); + tableDecorator.setRemoveAction(new RemoveLocationAction()); + tableDecorator.setEditActionUpdater(new EnableWhenSelected()); + tableDecorator.setRemoveActionUpdater(new EnableWhenSelectedAndRemovable()); + tableDecorator.setPreferredSize(DECORATOR_DIMENSIONS); + + final JPanel container = new JPanel(new BorderLayout()); + container.add(new TitledSeparator("Configuration File"), BorderLayout.NORTH); + container.add(tableDecorator.createPanel(), BorderLayout.CENTER); + final JLabel infoLabel = new JLabel( + "The active rules file may be overridden, or deactivated, by module settings.", + AllIcons.General.Information, SwingConstants.LEFT); + infoLabel.setBorder(JBUI.Borders.empty(8, 0, 4, 0)); + container.add(infoLabel, BorderLayout.SOUTH); + return container; + } + + private void setColumnWith( + final JTable table, + final int columnIndex, + final int minSize, + final int preferredSize, + final Integer maxSize) + { + final TableColumn column = table.getColumnModel().getColumn(columnIndex); + column.setMinWidth(minSize); + column.setWidth(preferredSize); + column.setPreferredWidth(preferredSize); + if(maxSize != null) + { + column.setMaxWidth(maxSize); + } + } + + public LocationTableModel locationModel() + { + return this.locationModel; + } + + class AddLocationAction implements AnActionButtonRunnable + { + private final Project project; + private final PMDConfigPanel pmdConfigPanel; + + AddLocationAction(final Project project, final PMDConfigPanel pmdConfigPanel) + { + this.project = project; + this.pmdConfigPanel = pmdConfigPanel; + } + + @Override + public void run(final AnActionButton anActionButton) + { + final LocationDialog dialogue = new LocationDialog( + (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, this.pmdConfigPanel), + this.project); + + if(dialogue.showAndGet()) + { + final ConfigurationLocation newLocation = dialogue.getConfigurationLocation(); + if(LocationManager.this.locationModel.getLocations().contains(newLocation)) + { + Messages.showWarningDialog( + this.project, + "This location has already been added", + "Duplicate Location"); + } + else + { + LocationManager.this.locationModel.addLocation(dialogue.getConfigurationLocation()); + } + } + } + } + + + class RemoveLocationAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final int selectedIndex = LocationManager.this.locationTable.getSelectedRow(); + if(selectedIndex == -1) + { + return; + } + + LocationManager.this.locationModel.removeLocationAt(selectedIndex); + } + } + + + class EnableWhenSelectedAndRemovable implements AnActionButtonUpdater + { + @Override + public boolean isEnabled(@NotNull final AnActionEvent e) + { + final int selectedItem = LocationManager.this.locationTable.getSelectedRow(); + return selectedItem >= 0 + && LocationManager.this.locationModel.getLocationAt(selectedItem).isRemovable(); + } + } + + + class EnableWhenSelected implements AnActionButtonUpdater + { + @Override + public boolean isEnabled(@NotNull final AnActionEvent e) + { + return LocationManager.this.locationTable.getSelectedRow() >= 0; + } + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/LocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java similarity index 98% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java index d9849b8..4c69bd1 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/LocationPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java @@ -1,8 +1,8 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.location; import static software.xdev.pmd.model.config.ConfigurationType.LOCAL_FILE; import static software.xdev.pmd.model.config.ConfigurationType.PROJECT_RELATIVE; -import static software.xdev.pmd.ui.config.project.LocationPanel.LocationType.FILE; +import static software.xdev.pmd.ui.config.project.location.LocationPanel.LocationType.FILE; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java similarity index 98% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationTableModel.java rename to src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java index 8745e1b..9671035 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/LocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project; +package software.xdev.pmd.ui.config.project.location; import static java.util.function.Predicate.not; From e47eab1571b53f9a8b33c1bdd8347f8de612f239 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 08:49:45 +0200 Subject: [PATCH 03/30] Refactor how locations are handled No longer store relative and absolute path in the same place (doesn't work) and also remove templating using $PROJECT_DIR$ --- .../config/{ => plugin}/PatternContainer.java | 2 +- .../PMDMavenAfterImportConfigurator.java | 4 +- .../model/config/ConfigurationLocation.java | 42 ++---- .../config/ConfigurationLocationFactory.java | 9 -- .../model/config/bundled/BundledConfig.java | 12 +- .../bundled/BundledConfigurationLocation.java | 20 ++- .../file/FileConfigurationLocation.java | 40 ++++-- .../RelativeFileConfigurationLocation.java | 21 +-- .../pmd/ui/config/project/PMDConfigPanel.java | 20 +-- .../components/SubPMDConfigPanelManager.java | 22 +++ .../exclusion/FileMaskPanelManager.java | 4 +- .../rulefilelocation}/CompletePanel.java | 2 +- .../rulefilelocation}/ErrorPanel.java | 2 +- .../rulefilelocation}/LocationDialog.java | 16 ++- .../rulefilelocation}/LocationPanel.java | 118 ++++++++------- .../LocationPanelManager.java} | 45 +++--- .../rulefilelocation}/LocationTableModel.java | 4 +- .../xdev/pmd/util/io/ProjectFilePaths.java | 135 ++---------------- 18 files changed, 219 insertions(+), 299 deletions(-) rename src/main/java/software/xdev/pmd/config/{ => plugin}/PatternContainer.java (97%) create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/SubPMDConfigPanelManager.java rename src/main/java/software/xdev/pmd/ui/config/project/{ => components}/exclusion/FileMaskPanelManager.java (97%) rename src/main/java/software/xdev/pmd/ui/config/project/{location => components/rulefilelocation}/CompletePanel.java (89%) rename src/main/java/software/xdev/pmd/ui/config/project/{location => components/rulefilelocation}/ErrorPanel.java (96%) rename src/main/java/software/xdev/pmd/ui/config/project/{location => components/rulefilelocation}/LocationDialog.java (94%) rename src/main/java/software/xdev/pmd/ui/config/project/{location => components/rulefilelocation}/LocationPanel.java (75%) rename src/main/java/software/xdev/pmd/ui/config/project/{location/LocationManager.java => components/rulefilelocation/LocationPanelManager.java} (75%) rename src/main/java/software/xdev/pmd/ui/config/project/{location => components/rulefilelocation}/LocationTableModel.java (96%) diff --git a/src/main/java/software/xdev/pmd/config/PatternContainer.java b/src/main/java/software/xdev/pmd/config/plugin/PatternContainer.java similarity index 97% rename from src/main/java/software/xdev/pmd/config/PatternContainer.java rename to src/main/java/software/xdev/pmd/config/plugin/PatternContainer.java index 20033cf..f5cf63c 100644 --- a/src/main/java/software/xdev/pmd/config/PatternContainer.java +++ b/src/main/java/software/xdev/pmd/config/plugin/PatternContainer.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.config; +package software.xdev.pmd.config.plugin; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java b/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java index e3aa8df..e9e043f 100644 --- a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java +++ b/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java @@ -198,9 +198,7 @@ private ConfigurationLocation createFileBasedConfigurationLocation(final Project final ConfigurationLocation configurationLocation = absolutePath ? new FileConfigurationLocation(project, id) : new RelativeFileConfigurationLocation(project, id); - configurationLocation.setLocation(absolutePath - ? s - : "$PROJECT_DIR$/" + s); + configurationLocation.setLocation(s); configurationLocation.setDescription(s); return configurationLocation; diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java index 83f5f19..aacb4fa 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java @@ -27,8 +27,6 @@ public abstract class ConfigurationLocation implements Comparable instanceDeduplicationCache = new ConcurrentReferenceHashMap<>(ConcurrentReferenceHashMap.ReferenceType.WEAK); - /** - * Create a new location. - * - * @param project the project this location is associated with. - * @param type the type. - * @param location the location. - * @param description the optional description. - * @return the location. - */ public @NotNull ConfigurationLocation create( final Project project, final String id, diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java index 7441232..c06f060 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java +++ b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java @@ -16,7 +16,6 @@ import software.xdev.pmd.langversion.LanguageVersionResolverService; import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationType; public final class BundledConfig @@ -65,9 +64,14 @@ public String getDescription() public boolean matches(@NotNull final ConfigurationLocation configurationLocation) { - return configurationLocation.getType() == ConfigurationType.BUNDLED - && Objects.equals(configurationLocation.getLocation(), this.location) - && Objects.equals(configurationLocation.getDescription(), this.description); + if(!(configurationLocation instanceof final BundledConfigurationLocation bundledConfigurationLocation)) + { + return false; + } + + final BundledConfig otherConfig = bundledConfigurationLocation.getBundledConfig(); + return Objects.equals(this.getLocation(), otherConfig.getLocation()) + && Objects.equals(this.getDescription(), otherConfig.getDescription()); } private static final AtomicInteger UNKNOWN_COUNTER = new AtomicInteger(1000); diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java index 097fcd8..b33a8aa 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java @@ -23,8 +23,6 @@ public BundledConfigurationLocation( @NotNull final Project project) { super(bundledConfig.getId(), ConfigurationType.BUNDLED, project); - super.setLocation(bundledConfig.getLocation()); - super.setDescription(bundledConfig.getDescription()); this.bundledConfig = bundledConfig; } @@ -38,13 +36,25 @@ public BundledConfig getBundledConfig() @Override public void setLocation(final String location) { - // bundled + // noop + } + + @Override + public String getLocation() + { + return "(bundled)"; } @Override public void setDescription(@Nullable final String description) { - // bundled + // noop + } + + @Override + public String getDescription() + { + return this.bundledConfig.getDescription(); } @Override @@ -57,7 +67,7 @@ public void validate() @Override protected synchronized RuleSet loadRuleSet() { - return new RuleSetLoader().loadFromResource(this.getLocation()); + return new RuleSetLoader().loadFromResource(this.getBundledConfig().getLocation()); } @Override diff --git a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java index fa7ed22..eeaef21 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java @@ -23,8 +23,11 @@ */ public class FileConfigurationLocation extends ConfigurationLocation { - private long nextReloadRuleSetMs; - private Instant lastModifiedFileTime; + protected long nextReloadRuleSetMs; + protected Instant lastModifiedFileTime; + protected String location; + protected Path locationPath; + protected String description; public FileConfigurationLocation( @NotNull final Project project, @@ -44,23 +47,36 @@ public FileConfigurationLocation( @Override public String getLocation() { - return this.projectFilePaths().detokenize(super.getLocation()); + return this.location; } @Override public void setLocation(final String location) { - if(location == null || location.isBlank()) - { - throw new IllegalArgumentException("A non-blank location is required"); - } - - super.setLocation(this.projectFilePaths().tokenise(location)); + this.location = location; + this.locationPath = this.getLocationPath(); + } + + protected String getRealLocation() + { + return this.projectFilePaths().toSystemPath(this.getLocation()); } protected Path getLocationPath() { - return Paths.get(this.getLocation()); + return Paths.get(this.getRealLocation()); + } + + @Override + public void setDescription(final String description) + { + this.description = description; + } + + @Override + public String getDescription() + { + return this.description; } @Nullable @@ -68,7 +84,7 @@ protected Instant lastModifiedTimeFromLocation() { try { - return Files.getLastModifiedTime(this.getLocationPath()).toInstant(); + return Files.getLastModifiedTime(this.locationPath).toInstant(); } catch(final IOException e) { @@ -84,7 +100,7 @@ protected synchronized RuleSet loadRuleSet() throws IOException final RuleSet ruleSet = new RuleSetLoader().loadFromString( this.getLocation(), - new String(Files.readAllBytes(this.getLocationPath()))); + new String(Files.readAllBytes(this.locationPath))); this.lastModifiedFileTime = this.lastModifiedTimeFromLocation(); return ruleSet; } diff --git a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java index 2cf4260..1c5bb6f 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java @@ -5,7 +5,6 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.model.config.ConfigurationType; -import software.xdev.pmd.util.io.ProjectFilePaths; /** @@ -14,6 +13,8 @@ */ public class RelativeFileConfigurationLocation extends FileConfigurationLocation { + private static final String LEGACY_IDEA_PROJECT_DIR_START = "$PROJECT_DIR$/"; + public RelativeFileConfigurationLocation( @NotNull final Project project, @NotNull final String id) @@ -30,14 +31,14 @@ public boolean canBeResolvedInDefaultProject() @Override public void setLocation(final String location) { - if(location == null || location.isBlank()) - { - throw new IllegalArgumentException("A non-blank location is required"); - } - - final ProjectFilePaths projectFilePaths = this.projectFilePaths(); - super.setLocation(projectFilePaths.tokenise( - projectFilePaths.makeProjectRelative( - projectFilePaths.detokenize(location)))); + super.setLocation(location.startsWith(LEGACY_IDEA_PROJECT_DIR_START) + ? location.substring(LEGACY_IDEA_PROJECT_DIR_START.length()) + : location); + } + + @Override + protected String getRealLocation() + { + return this.projectFilePaths().makeProjectRelativePathAbsolute(super.getRealLocation()); } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index abfa35b..d9bb685 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -28,8 +28,8 @@ import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; -import software.xdev.pmd.ui.config.project.exclusion.FileMaskPanelManager; -import software.xdev.pmd.ui.config.project.location.LocationManager; +import software.xdev.pmd.ui.config.project.components.exclusion.FileMaskPanelManager; +import software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanelManager; /** @@ -47,7 +47,7 @@ public class PMDConfigPanel extends JPanel private final JBCheckBox chbxUseCacheFile = new JBCheckBox("Use cache file"); private final JBCheckBox chbxImportSettingsFromMaven = new JBCheckBox("Import settings from Maven"); - private final LocationManager locationManager = new LocationManager(); + private final LocationPanelManager rulefileLocationPanelManager; private final FileMaskPanelManager exclusionPanelManager = new FileMaskPanelManager( "Exclusions", @@ -68,6 +68,8 @@

Ignores certain files (patterns). """ ); + private final ThirdPartyClasspathPanelManager thirdPartyClasspathPanelManager; + final Project project; public PMDConfigPanel(@NotNull final Project project) @@ -76,6 +78,8 @@ public PMDConfigPanel(@NotNull final Project project) this.project = project; + this.rulefileLocationPanelManager = new LocationPanelManager(project, this); + this.initialise(); } @@ -114,7 +118,7 @@ private JPanel buildConfigPanel() this.createDefaultGridBagConstraints(0, 2, 2)); configFilePanel.add( - this.locationManager.buildRuleFilePanel(this.project, this), + this.rulefileLocationPanelManager.buildPanel(), this.createFullWidthGridBagConstraints(3, 1.0)); configFilePanel.add( @@ -175,8 +179,8 @@ public void showPluginConfiguration(@NotNull final PluginConfiguration pluginCon this.chbxShowSuppressedWarnings.setSelected(pluginConfig.showSuppressedWarnings()); this.chbxUseCacheFile.setSelected(pluginConfig.useCacheFile()); this.chbxImportSettingsFromMaven.setSelected(pluginConfig.importSettingsFromMaven()); - this.locationManager.locationModel().setLocations(new ArrayList<>(pluginConfig.locations())); - this.locationManager.locationModel().setActiveLocations(pluginConfig.getActiveLocations()); + this.rulefileLocationPanelManager.locationModel().setLocations(new ArrayList<>(pluginConfig.locations())); + this.rulefileLocationPanelManager.locationModel().setActiveLocations(pluginConfig.getActiveLocations()); this.exclusionPanelManager.update(pluginConfig.projectRelativeFileExclusions().stream() .map(PatternContainer::patternString) .collect(Collectors.toCollection(TreeSet::new))); @@ -192,8 +196,8 @@ public PluginConfiguration getPluginConfiguration() (ScanScope)this.cbScope.getSelectedItem(), ScanScope::getDefaultValue)) .withProjectRelativeFileExclusionsRaw(this.exclusionPanelManager.getPatterns()) - .withLocations(new TreeSet<>(this.locationManager.locationModel().getLocations())) - .withActiveLocationIds(this.locationManager.locationModel().getActiveLocations().stream() + .withLocations(new TreeSet<>(this.rulefileLocationPanelManager.locationModel().getLocations())) + .withActiveLocationIds(this.rulefileLocationPanelManager.locationModel().getActiveLocations().stream() .map(ConfigurationLocation::getId) .collect(Collectors.toCollection(TreeSet::new))) .withImportSettingFromMaven(this.chbxImportSettingsFromMaven.isSelected()) diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/SubPMDConfigPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/SubPMDConfigPanelManager.java new file mode 100644 index 0000000..fb2d118 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/SubPMDConfigPanelManager.java @@ -0,0 +1,22 @@ +package software.xdev.pmd.ui.config.project.components; + +import java.awt.Dimension; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.ui.config.project.PMDConfigPanel; + + +public abstract class SubPMDConfigPanelManager +{ + protected static final Dimension DECORATOR_DIMENSIONS = new Dimension(300, 50); + + protected final Project project; + protected final PMDConfigPanel pmdConfigPanel; + + public SubPMDConfigPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) + { + this.project = project; + this.pmdConfigPanel = pmdConfigPanel; + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/exclusion/FileMaskPanelManager.java similarity index 97% rename from src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/exclusion/FileMaskPanelManager.java index 6504569..74f990e 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/exclusion/FileMaskPanelManager.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/exclusion/FileMaskPanelManager.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.exclusion; +package software.xdev.pmd.ui.config.project.components.exclusion; import java.awt.BorderLayout; import java.awt.Dimension; @@ -19,7 +19,7 @@ import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBList; -import software.xdev.pmd.config.PatternContainer; +import software.xdev.pmd.config.plugin.PatternContainer; public class FileMaskPanelManager diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java similarity index 89% rename from src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java index 94121f8..0a46d57 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/CompletePanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java similarity index 96% rename from src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java index a1c59f2..4fbc473 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/ErrorPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import static javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS; import static javax.swing.JScrollPane.VERTICAL_SCROLLBAR_ALWAYS; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java similarity index 94% rename from src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java index c57176a..34f2a9a 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import java.awt.BorderLayout; import java.awt.Dialog; @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; @@ -31,6 +32,8 @@ @SuppressWarnings("checkstyle:MagicNumber") public class LocationDialog extends DialogWrapper { + private static final Logger LOG = Logger.getInstance(LocationDialog.class); + private static final Insets COMPONENT_INSETS = JBUI.insets(4); private static final int WIDTH = 500; private static final int HEIGHT = 400; @@ -226,7 +229,16 @@ void onNext() switch(this.currentStep) { case SELECT: - location = this.locationPanel.getConfigurationLocation(); + try + { + location = this.locationPanel.getConfigurationLocation(); + } + catch(final Exception ex) + { + this.showError("Failed to get configuration: " + ex.getMessage()); + LOG.debug("Failed to get configuration", ex); + return; + } if(location == null) { this.showError("No location has been entered"); diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java similarity index 75% rename from src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java index 4c69bd1..60fc097 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java @@ -1,8 +1,8 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import static software.xdev.pmd.model.config.ConfigurationType.LOCAL_FILE; import static software.xdev.pmd.model.config.ConfigurationType.PROJECT_RELATIVE; -import static software.xdev.pmd.ui.config.project.location.LocationPanel.LocationType.FILE; +import static software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanel.LocationType.FILE; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -10,7 +10,11 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; import javax.swing.AbstractAction; @@ -36,6 +40,7 @@ import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.config.ConfigurationLocationFactory; import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.util.io.ProjectFilePaths; @SuppressWarnings("checkstyle:MagicNumber") @@ -138,11 +143,9 @@ private void enabledLocation(final LocationType locationType) private ConfigurationType typeOfFile() { - if(this.relativeFileCheckbox.isSelected()) - { - return PROJECT_RELATIVE; - } - return LOCAL_FILE; + return this.relativeFileCheckbox.isSelected() + ? PROJECT_RELATIVE + : LOCAL_FILE; } /** @@ -154,52 +157,73 @@ public ConfigurationLocation getConfigurationLocation() { final String newId = UUID.randomUUID().toString(); - if(this.fileLocationField.isEnabled() && this.isNotBlank(this.fileLocation())) + if(this.fileLocationField.isEnabled() && this.isNotBlank(this.fileLocationField.getText())) { + final ConfigurationType type = this.typeOfFile(); return this.configurationLocationFactory().create( this.project, newId, - this.typeOfFile(), - this.fileLocation(), + type, + this.project.getService(ProjectFilePaths.class) + .toUnixPath(this.getFileLocationPath(type).toString()), this.descriptionField.getText()); } return null; } - private String fileLocation() + private Path getFileLocationPath(final ConfigurationType type) { final String filename = this.trim(this.fileLocationField.getText()); + if(filename == null || filename.isBlank()) + { + throw new IllegalArgumentException("Invalid path: " + filename); + } - if(new File(filename).exists()) + final Path path = Paths.get(filename); + if(path.isAbsolute()) { - return filename; + // Handle absolute path + if(!Files.exists(path)) + { + throw new IllegalArgumentException("Invalid path: " + path); + } + + if(type != PROJECT_RELATIVE) + { + return path; + } + + // Make project relative + return this.guessProjectNioPath().relativize(path); } - final File projectRelativePath = this.projectRelativeFileOf(filename); - if(projectRelativePath.exists()) + // Handle relative path + // Validate that the file exists + final Path absolutePath = this.guessProjectNioPath() + .resolve(path) + .normalize() + .toAbsolutePath(); + + if(!Files.exists(absolutePath)) { - return projectRelativePath.getAbsolutePath(); + throw new IllegalArgumentException("Invalid path: " + absolutePath); } - return filename; + return type == PROJECT_RELATIVE ? path : absolutePath; } - private File projectRelativeFileOf(final String filename) + private Path guessProjectNioPath() { - return Paths.get(new File(this.project.getBasePath(), filename).getAbsolutePath()) - .normalize() - .toAbsolutePath() - .toFile(); + return Objects.requireNonNull( + ProjectUtil.guessProjectDir(this.project), + "Unable to determine project dir") + .toNioPath(); } private String trim(final String text) { - if(text != null) - { - return text.trim(); - } - return null; + return text != null ? text.trim() : null; } private ConfigurationLocationFactory configurationLocationFactory() @@ -212,30 +236,6 @@ private boolean isNotBlank(final String str) return str != null && !str.isBlank(); } - /** - * Set the configuration location. - * - * @param configurationLocation the location. - */ - public void setConfigurationLocation(final ConfigurationLocation configurationLocation) - { - this.relativeFileCheckbox.setSelected(false); - - if(configurationLocation == null) - { - this.fileLocationRadio.setEnabled(true); - this.fileLocationField.setText(null); - } - else if(configurationLocation.getType() == LOCAL_FILE - || configurationLocation.getType() == PROJECT_RELATIVE) - { - this.fileLocationRadio.setEnabled(true); - this.fileLocationField.setText(configurationLocation.getLocation()); - this.relativeFileCheckbox.setSelected(configurationLocation.getType() == PROJECT_RELATIVE); - } - throw new IllegalArgumentException("Unsupported configuration type: " + configurationLocation.getType()); - } - private final class BrowseAction extends AbstractAction { BrowseAction() @@ -252,10 +252,18 @@ private final class BrowseAction extends AbstractAction @Override public void actionPerformed(final ActionEvent e) { - final String configFilePath = LocationPanel.this.fileLocation(); - final VirtualFile toSelect = (configFilePath != null && !configFilePath.isBlank()) - ? LocalFileSystem.getInstance().findFileByPath(configFilePath) - : ProjectUtil.guessProjectDir(LocationPanel.this.project); + Optional fileLocationPath; + try + { + fileLocationPath = Optional.ofNullable(LocationPanel.this.getFileLocationPath(LOCAL_FILE)); + } + catch(final Exception ex) + { + fileLocationPath = Optional.empty(); + } + final VirtualFile toSelect = fileLocationPath + .map(LocalFileSystem.getInstance()::findFileByNioFile) + .orElseGet(() -> ProjectUtil.guessProjectDir(LocationPanel.this.project)); final VirtualFile chosen = FileChooser.chooseFile( FileChooserDescriptorFactory.createSingleFileDescriptor("xml"), diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java similarity index 75% rename from src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java index f83400a..8dec1dc 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationManager.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java @@ -1,8 +1,7 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import java.awt.BorderLayout; import java.awt.Dialog; -import java.awt.Dimension; import javax.swing.JLabel; import javax.swing.JPanel; @@ -27,20 +26,25 @@ import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.ui.config.project.PMDConfigPanel; +import software.xdev.pmd.ui.config.project.components.SubPMDConfigPanelManager; -public class LocationManager +public class LocationPanelManager extends SubPMDConfigPanelManager { private static final int ACTIVE_COL_MIN_WIDTH = 40; private static final int ACTIVE_COL_MAX_WIDTH = 55; private static final int DESC_COL_MIN_WIDTH = 100; private static final int DESC_COL_MAX_WIDTH = 200; - private static final Dimension DECORATOR_DIMENSIONS = new Dimension(300, 50); final LocationTableModel locationModel = new LocationTableModel(); final JBTable locationTable = new JBTable(this.locationModel); - public JPanel buildRuleFilePanel(final Project project, final PMDConfigPanel pmdConfigPanel) + public LocationPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) + { + super(project, pmdConfigPanel); + } + + public JPanel buildPanel() { this.setColumnWith(this.locationTable, 0, ACTIVE_COL_MIN_WIDTH, ACTIVE_COL_MAX_WIDTH, ACTIVE_COL_MAX_WIDTH); this.setColumnWith(this.locationTable, 1, DESC_COL_MIN_WIDTH, DESC_COL_MAX_WIDTH, DESC_COL_MAX_WIDTH); @@ -49,7 +53,7 @@ public JPanel buildRuleFilePanel(final Project project, final PMDConfigPanel pmd this.locationTable.getTableHeader().setReorderingAllowed(false); final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable); - tableDecorator.setAddAction(new AddLocationAction(project, pmdConfigPanel)); + tableDecorator.setAddAction(new AddLocationAction()); tableDecorator.setRemoveAction(new RemoveLocationAction()); tableDecorator.setEditActionUpdater(new EnableWhenSelected()); tableDecorator.setRemoveActionUpdater(new EnableWhenSelectedAndRemovable()); @@ -90,35 +94,26 @@ public LocationTableModel locationModel() class AddLocationAction implements AnActionButtonRunnable { - private final Project project; - private final PMDConfigPanel pmdConfigPanel; - - AddLocationAction(final Project project, final PMDConfigPanel pmdConfigPanel) - { - this.project = project; - this.pmdConfigPanel = pmdConfigPanel; - } - @Override public void run(final AnActionButton anActionButton) { final LocationDialog dialogue = new LocationDialog( - (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, this.pmdConfigPanel), - this.project); + (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, LocationPanelManager.this.pmdConfigPanel), + LocationPanelManager.this.project); if(dialogue.showAndGet()) { final ConfigurationLocation newLocation = dialogue.getConfigurationLocation(); - if(LocationManager.this.locationModel.getLocations().contains(newLocation)) + if(LocationPanelManager.this.locationModel.getLocations().contains(newLocation)) { Messages.showWarningDialog( - this.project, + LocationPanelManager.this.project, "This location has already been added", "Duplicate Location"); } else { - LocationManager.this.locationModel.addLocation(dialogue.getConfigurationLocation()); + LocationPanelManager.this.locationModel.addLocation(dialogue.getConfigurationLocation()); } } } @@ -130,13 +125,13 @@ class RemoveLocationAction implements AnActionButtonRunnable @Override public void run(final AnActionButton anActionButton) { - final int selectedIndex = LocationManager.this.locationTable.getSelectedRow(); + final int selectedIndex = LocationPanelManager.this.locationTable.getSelectedRow(); if(selectedIndex == -1) { return; } - LocationManager.this.locationModel.removeLocationAt(selectedIndex); + LocationPanelManager.this.locationModel.removeLocationAt(selectedIndex); } } @@ -146,9 +141,9 @@ class EnableWhenSelectedAndRemovable implements AnActionButtonUpdater @Override public boolean isEnabled(@NotNull final AnActionEvent e) { - final int selectedItem = LocationManager.this.locationTable.getSelectedRow(); + final int selectedItem = LocationPanelManager.this.locationTable.getSelectedRow(); return selectedItem >= 0 - && LocationManager.this.locationModel.getLocationAt(selectedItem).isRemovable(); + && LocationPanelManager.this.locationModel.getLocationAt(selectedItem).isRemovable(); } } @@ -158,7 +153,7 @@ class EnableWhenSelected implements AnActionButtonUpdater @Override public boolean isEnabled(@NotNull final AnActionEvent e) { - return LocationManager.this.locationTable.getSelectedRow() >= 0; + return LocationPanelManager.this.locationTable.getSelectedRow() >= 0; } } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java similarity index 96% rename from src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java index 9671035..cc77779 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/location/LocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.location; +package software.xdev.pmd.ui.config.project.components.rulefilelocation; import static java.util.function.Predicate.not; @@ -182,7 +182,7 @@ public Object getValueAt(final int rowIndex, final int columnIndex) { case COLUMN_ACTIVE -> this.activeLocations.contains(this.locations.get(rowIndex)); case COLUMN_DESCRIPTION -> this.locations.get(rowIndex).getDescription(); - case COLUMN_LOCATION -> this.locations.get(rowIndex).getRawLocation(); + case COLUMN_LOCATION -> this.locations.get(rowIndex).getLocation(); default -> throw new IllegalArgumentException("Invalid column: " + columnIndex); }; } diff --git a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java index af74f95..3166b9a 100644 --- a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java +++ b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java @@ -1,12 +1,9 @@ package software.xdev.pmd.util.io; import java.io.File; -import java.util.function.Function; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -14,130 +11,39 @@ public class ProjectFilePaths { - private static final Logger LOG = Logger.getInstance(ProjectFilePaths.class); - - private static final String IDEA_PROJECT_DIR = "$PROJECT_DIR$"; - private final Project project; private final char separatorChar; - private final Function absolutePathOf; public ProjectFilePaths(@NotNull final Project project) { - this(project, File.separatorChar, File::getAbsolutePath); + this(project, File.separatorChar); } private ProjectFilePaths( @NotNull final Project project, - final char separatorChar, - @NotNull final Function absolutePathOf) + final char separatorChar) { this.project = project; this.separatorChar = separatorChar; - this.absolutePathOf = absolutePathOf; } - @Nullable - public String makeProjectRelative(@Nullable final String path) + public String makeProjectRelativePathAbsolute(final String path) { if(path == null || this.project.isDefault()) { - return path; + throw new IllegalArgumentException("Invalid path or project"); } - final File projectPath = this.projectPath(); - if(projectPath == null) + final VirtualFile projectDir = ProjectUtil.guessProjectDir(this.project); + if(projectDir == null) { - LOG.debug("Couldn't find project path, returning full path: " + path); - return path; + throw new IllegalStateException("Unable to guess directory of project " + this.project); } - try - { - final String basePath = this.absolutePathOf.apply(projectPath) + this.separatorChar; - return basePath + FilePaths.relativePath(path, basePath, String.valueOf(this.separatorChar)); - } - catch(final FilePaths.PathResolutionException e) - { - LOG.debug("No common path was found between " + path + " and " + projectPath.getAbsolutePath()); - return path; - } - catch(final Exception e) - { - LOG.warn("Failed to make relative: " + path, e); - return path; - } + return projectDir.toNioPath().toAbsolutePath().toString() + this.separatorChar + path; } - @Nullable - public String tokenise(@Nullable final String fsPath) - { - if(fsPath == null) - { - return null; - } - - if(this.project.isDefault()) - { - if(new File(fsPath).exists() || fsPath.startsWith(IDEA_PROJECT_DIR)) - { - return this.toUnixPath(fsPath); - } - else - { - return IDEA_PROJECT_DIR + this.toUnixPath(this.separatorChar + fsPath); - } - } - - final File projectPath = this.projectPath(); - if(projectPath != null && fsPath.startsWith(this.absolutePathOf.apply(projectPath) + this.separatorChar)) - { - return IDEA_PROJECT_DIR - + this.toUnixPath(fsPath.substring(this.absolutePathOf.apply(projectPath).length())); - } - - return this.toUnixPath(fsPath); - } - - @Nullable - public String detokenize(@Nullable final String tokenisedPath) - { - if(tokenisedPath == null) - { - return null; - } - - String detokenisedPath = this.replaceProjectToken(tokenisedPath); - - if(detokenisedPath == null) - { - detokenisedPath = this.toSystemPath(tokenisedPath); - } - return detokenisedPath; - } - - private String replaceProjectToken(final String path) - { - final int prefixLocation = path.indexOf(IDEA_PROJECT_DIR); - if(prefixLocation >= 0) - { - final File projectPath = this.projectPath(); - if(projectPath != null) - { - final String projectRelativePath = - this.toSystemPath(path.substring(prefixLocation + IDEA_PROJECT_DIR.length())); - final String completePath = projectPath + File.separator + projectRelativePath; - return this.absolutePathOf.apply(new File(completePath)); - } - else - { - LOG.warn("Could not detokenize path as project dir is unset: " + path); - } - } - return null; - } - - private String toUnixPath(final String systemPath) + public String toUnixPath(final String systemPath) { if(this.separatorChar == '/') { @@ -146,7 +52,7 @@ private String toUnixPath(final String systemPath) return systemPath.replace(this.separatorChar, '/'); } - private String toSystemPath(final String unixPath) + public String toSystemPath(final String unixPath) { if(this.separatorChar == '/') { @@ -154,25 +60,4 @@ private String toSystemPath(final String unixPath) } return unixPath.replace('/', this.separatorChar); } - - @Nullable - private File projectPath() - { - try - { - final VirtualFile baseDir = ProjectUtil.guessProjectDir(this.project); - if(baseDir == null) - { - return null; - } - - return new File(baseDir.getPath()); - } - catch(final Exception e) - { - // IDEA 10.5.2 sometimes throws an AssertionException in project.getBaseDir() - LOG.debug("Couldn't retrieve base location", e); - return null; - } - } } From 843a5b12d4e853daed2912af14c90d25021af09f Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 08:50:38 +0200 Subject: [PATCH 04/30] Bump version --- CHANGELOG.md | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index add7a17..70ce4db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ -# 1.2.0 +# 2.0.0 * Make it possible to add (project-wide) exclusions #58 * Add support for importing settings from Maven PMD plugin (best-effort) * Add shortcuts for jumping to previous/next result/violation * Add a "scan entire project" button #59 * Only show "Enable automatic build" hint on startup when projects have PMD enabled +* Refactor location store # 1.1.4 * Prevent rare NPE in module configuration UI #107 diff --git a/gradle.properties b/gradle.properties index b72d044..ce05bf1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ pluginGroup=software.xdev.pmd pluginName=PMD X # SemVer format -> https://semver.org -pluginVersion=1.2.0-SNAPSHOT +pluginVersion=2.0.0-SNAPSHOT # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType=IU platformVersion=2026.2 From e2dc18518102f469466f4d37cacbfb9d73b400d0 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 09:56:50 +0200 Subject: [PATCH 05/30] Correctly handle resolved "$PROJECT_DIR$" --- .../RelativeFileConfigurationLocation.java | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java index 1c5bb6f..adc68e1 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; +import com.intellij.openapi.components.PathMacroManager; import com.intellij.openapi.project.Project; import software.xdev.pmd.model.config.ConfigurationType; @@ -13,7 +14,7 @@ */ public class RelativeFileConfigurationLocation extends FileConfigurationLocation { - private static final String LEGACY_IDEA_PROJECT_DIR_START = "$PROJECT_DIR$/"; + private static final String LEGACY_IDEA_PROJECT_DIR = "$PROJECT_DIR$"; public RelativeFileConfigurationLocation( @NotNull final Project project, @@ -28,12 +29,31 @@ public boolean canBeResolvedInDefaultProject() return false; } + @SuppressWarnings("checkstyle:FinalParameters") @Override - public void setLocation(final String location) + public void setLocation(String location) { - super.setLocation(location.startsWith(LEGACY_IDEA_PROJECT_DIR_START) - ? location.substring(LEGACY_IDEA_PROJECT_DIR_START.length()) - : location); + if(location.startsWith(LEGACY_IDEA_PROJECT_DIR) && location.length() > LEGACY_IDEA_PROJECT_DIR.length() + 1) + { + location = location.substring(LEGACY_IDEA_PROJECT_DIR.length() + 1); + } + // Detect legacy $PROJECT_DIR$ that was resolved during importing + if(location.length() > 5 + // linux e.g. /abc/... + && (location.startsWith("/") + // windows e.g. c:/abc/... + || location.charAt(1) == ':' && location.charAt(2) == '/')) + { + final String resolvedProjectDir = PathMacroManager.getInstance(this.getProject()) + .expandPath(LEGACY_IDEA_PROJECT_DIR); + if(location.startsWith(resolvedProjectDir) && location.length() > resolvedProjectDir.length() + 1) + { + // Also cut away path separator + location = location.substring(resolvedProjectDir.length() + 1); + } + } + + super.setLocation(location); } @Override From b8e515ed4d089d4598bc94e3537d7762d6555f92 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 10:37:50 +0200 Subject: [PATCH 06/30] Order BundledLocations last This way the UI is cleaner and the user configured options can be found easier --- CHANGELOG.md | 1 + .../xdev/pmd/model/config/ConfigurationLocation.java | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ce4db..60a3ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add a "scan entire project" button #59 * Only show "Enable automatic build" hint on startup when projects have PMD enabled * Refactor location store +* Order bundled locations at the end # 1.1.4 * Prevent rare NPE in module configuration UI #107 diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java index aacb4fa..3f1aea3 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java @@ -151,7 +151,7 @@ public String toString() @Override public int compareTo(@NotNull final ConfigurationLocation other) { - // bundled configs go first, ordered by their position in the BundledConfig enum + // bundled configs go last, ordered by their position in the BundledConfig enum if(other instanceof final BundledConfigurationLocation otherBundledConfigurationLocation) { if(this instanceof final BundledConfigurationLocation thisBundledConfigurationLocation) @@ -160,12 +160,12 @@ public int compareTo(@NotNull final ConfigurationLocation other) thisBundledConfigurationLocation.getBundledConfig().getSortOrder(), otherBundledConfigurationLocation.getBundledConfig().getSortOrder()); } - return 1; + return -1; } if(this instanceof BundledConfigurationLocation) { - return -1; + return 1; } int result = this.compareStrings(this.getDescription(), other.getDescription()); From 52b6c51794b2dba87d31787db91ab741b7156429 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 12:37:13 +0200 Subject: [PATCH 07/30] Path is automatically templated by IntelliJ --- .../model/config/file/RelativeFileConfigurationLocation.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java index adc68e1..536010c 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java @@ -33,10 +33,6 @@ public boolean canBeResolvedInDefaultProject() @Override public void setLocation(String location) { - if(location.startsWith(LEGACY_IDEA_PROJECT_DIR) && location.length() > LEGACY_IDEA_PROJECT_DIR.length() + 1) - { - location = location.substring(LEGACY_IDEA_PROJECT_DIR.length() + 1); - } // Detect legacy $PROJECT_DIR$ that was resolved during importing if(location.length() > 5 // linux e.g. /abc/... From daf60dd686bd8b8c1502d0b114f2adbbc819c348 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 12:43:11 +0200 Subject: [PATCH 08/30] Disable project configuration when project is not a real project --- .../ui/config/project/PMDConfigurable.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java index c5bd6fc..da14443 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java @@ -1,6 +1,7 @@ package software.xdev.pmd.ui.config.project; import javax.swing.JComponent; +import javax.swing.JLabel; import org.jetbrains.annotations.NotNull; @@ -25,7 +26,8 @@ public class PMDConfigurable implements Configurable { this.pluginConfigurationManager = project.getService(PluginConfigurationManager.class); - this.configPanel = new PMDConfigPanel(project); + // Default project (start screen) is not supported! + this.configPanel = !project.isDefault() ? new PMDConfigPanel(project) : null; } @Override @@ -43,6 +45,11 @@ public String getHelpTopic() @Override public JComponent createComponent() { + if(this.configPanel == null) + { + return new JLabel("Project configuration not available"); + } + this.reset(); return this.configPanel; } @@ -55,6 +62,11 @@ private PluginConfiguration getConfigPanelPluginConfig() @Override public boolean isModified() { + if(this.configPanel == null) + { + return false; + } + return !this.pluginConfigurationManager.getCurrent() // Old .isIdentical(this.getConfigPanelPluginConfig()); // New } @@ -62,14 +74,23 @@ public boolean isModified() @Override public void apply() { + if(this.configPanel == null) + { + return; + } + this.pluginConfigurationManager.setCurrent(this.getConfigPanelPluginConfig()); } @Override public void reset() { - final PluginConfiguration pluginConfig = this.pluginConfigurationManager.getCurrent(); - this.configPanel.showPluginConfiguration(pluginConfig); + if(this.configPanel == null) + { + return; + } + + this.configPanel.showPluginConfiguration(this.pluginConfigurationManager.getCurrent()); } @Override From e1a85f1942a689545e163794c075ce5dc63205db Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 29 Jul 2026 17:04:40 +0200 Subject: [PATCH 09/30] Implement basic support for loading 3rd party rules jars See also #10 --- CHANGELOG.md | 1 + .../xdev/pmd/analysis/PMDAnalyzer.java | 22 ++- .../analysis/ProjectScanClasspathManager.java | 67 +++++++ .../xdev/pmd/config/PluginConfiguration.java | 3 + .../config/PluginConfigurationBuilder.java | 21 ++- .../config/PluginConfigurationManager.java | 39 +++- .../ThirdPartyClasspathConfigContainer.java | 167 ++++++++++++++++++ .../state/project/ProjectSettingsState.java | 34 +++- .../model/config/ConfigurationLocation.java | 20 ++- .../bundled/BundledConfigurationLocation.java | 11 +- .../UnknownBundledConfigurationLocation.java | 4 +- .../file/FileConfigurationLocation.java | 28 ++- .../file/pmd/DefaultRuleSetLoaderCreator.java | 116 ++++++++++++ ...LoadFromStringRuleSetLoaderWorkaround.java | 135 ++++++++++++++ .../pmd/ui/config/project/PMDConfigPanel.java | 47 ++++- .../rulefilelocation/LocationDialog.java | 4 +- .../LocationPanelManager.java | 2 +- .../ThirdPartyClasspathPanelManager.java | 141 +++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + .../pmd/DefaultRuleSetLoaderCreatorTest.java | 17 ++ ...FromStringRuleSetLoaderWorkaroundTest.java | 15 ++ 21 files changed, 845 insertions(+), 50 deletions(-) create mode 100644 src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java create mode 100644 src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java create mode 100644 src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java create mode 100644 src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java create mode 100644 src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java create mode 100644 src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a3ab0..96c628b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Only show "Enable automatic build" hint on startup when projects have PMD enabled * Refactor location store * Order bundled locations at the end +* Implement support for 3rd party class path rules # 1.1.4 * Prevent rare NPE in module configuration UI #107 diff --git a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java index e68bbf3..3ed9f07 100644 --- a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java +++ b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java @@ -66,8 +66,8 @@ public class PMDAnalyzer implements Disposable private final Map, ReentrantLock> locks = new ConcurrentHashMap<>(); private final Map, CacheFile> cacheFiles = new ConcurrentHashMap<>(); // Reuse classloader when path is the same - private final Map, ClassLoader> cachedSdkLibAuxClassLoader = - new ConcurrentReferenceHashMap<>(); + private final Map, ClassLoader>> baseClassLoaderCachedSdkLibAuxClassLoaders = + new ConcurrentReferenceHashMap<>(ConcurrentReferenceHashMap.ReferenceType.WEAK); public PMDAnalyzer(final Project project) { @@ -135,11 +135,14 @@ private PMDAnalysisResult analyzeInternal( { final long startMs = System.currentTimeMillis(); + final ClassLoader baseClassLoader = + this.project.getService(ProjectScanClasspathManager.class).getClassLoader(); + // Load ruleset - if required - async in background final CompletableFuture> cfLoadRuleSetsAsync = CompletableFuture.supplyAsync( () -> configurationLocations.stream() - .map(ConfigurationLocation::getOrRefreshCachedRuleSet) + .map(configLoc -> configLoc.getOrRefreshCachedRuleSet(baseClassLoader)) .filter(Objects::nonNull) .toList(), RULESET_LOADER_SERVICE); @@ -173,7 +176,7 @@ private PMDAnalysisResult analyzeInternal( .map(List::of) .orElseGet(() -> List.of(ModuleManager.getInstance(this.project).getModules())); - pmdConfig.setClassLoader(this.classLoaderFor(modules)); + pmdConfig.setClassLoader(this.classLoaderFor(modules, baseClassLoader)); if(pluginConfiguration.showSuppressedWarnings()) { @@ -323,7 +326,9 @@ private Map> getHighestLanguageVersionAndFiles( } @NotNull - private ClasspathClassLoader classLoaderFor(final List modules) + private ClasspathClassLoader classLoaderFor( + final List modules, + final ClassLoader baseClassLoader) { final Set fullClassPaths = this.classPathFor(modules, UnaryOperator.identity()); final Set appClassPaths = this.classPathFor(modules, o -> o.withoutSdk().withoutLibraries()); @@ -331,9 +336,14 @@ private ClasspathClassLoader classLoaderFor(final List modules) .filter(s -> !appClassPaths.contains(s)) .collect(Collectors.toSet()); + final Map, ClassLoader> cachedSdkLibAuxClassLoaders = + this.baseClassLoaderCachedSdkLibAuxClassLoaders.computeIfAbsent( + baseClassLoader, + ignored -> new ConcurrentReferenceHashMap<>()); + return this.createClasspathClassLoader( appClassPaths, - this.cachedSdkLibAuxClassLoader.computeIfAbsent( + cachedSdkLibAuxClassLoaders.computeIfAbsent( sdkLibClassPaths, paths -> this.createClasspathClassLoader(paths, PMDConfiguration.class.getClassLoader()))); } diff --git a/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java b/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java new file mode 100644 index 0000000..e3219e7 --- /dev/null +++ b/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java @@ -0,0 +1,67 @@ +package software.xdev.pmd.analysis; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Objects; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Logger; + +import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; + + +public class ProjectScanClasspathManager implements Disposable +{ + private static final ClassLoader DEFAULT_CL = ProjectScanClasspathManager.class.getClassLoader(); + private static final Logger LOG = Logger.getInstance(ProjectScanClasspathManager.class); + + private ThirdPartyClasspathConfigContainer cachedConfigContainerKey; + private URLClassLoader classLoader; + + public void configure(final ThirdPartyClasspathConfigContainer configContainer) + { + if(!Objects.equals(this.cachedConfigContainerKey, configContainer)) + { + this.closeCurrentClassLoader(); + if(!configContainer.urls().isEmpty()) + { + this.classLoader = new URLClassLoader(configContainer.urls().toArray(URL[]::new), DEFAULT_CL); + } + + this.cachedConfigContainerKey = configContainer; + } + } + + public ClassLoader getClassLoader() + { + return this.classLoader != null ? this.classLoader : DEFAULT_CL; + } + + private void closeCurrentClassLoader() + { + closeClassLoader(this.classLoader); + this.classLoader = null; + } + + @Override + public void dispose() + { + this.closeCurrentClassLoader(); + } + + private static void closeClassLoader(final URLClassLoader classLoader) + { + if(classLoader != null) + { + try + { + classLoader.close(); + } + catch(final IOException e) + { + LOG.warn("Failed to close classloader", e); + } + } + } +} diff --git a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java index d7f9e76..0c9e4a5 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java @@ -11,6 +11,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import software.xdev.pmd.config.plugin.PatternContainer; +import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; @@ -27,6 +29,7 @@ public record PluginConfiguration( SortedSet projectRelativeFileExclusions, SortedSet locations, SortedSet activeLocationIds, + ThirdPartyClasspathConfigContainer thirdPartyClasspath, boolean importSettingsFromMaven, Cache cache ) diff --git a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java index 2950f25..0224ff3 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java @@ -12,6 +12,8 @@ import com.intellij.openapi.project.Project; +import software.xdev.pmd.config.plugin.PatternContainer; +import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.config.ConfigurationLocationFactory; import software.xdev.pmd.model.config.bundled.BundledConfig; @@ -27,6 +29,7 @@ public final class PluginConfigurationBuilder private SortedSet projectRelativeFileExclusions; private SortedSet locations; private SortedSet activeLocationIds; + private ThirdPartyClasspathConfigContainer thirdPartyClasspath; private boolean importSettingsFromMaven; public PluginConfigurationBuilder(final Project project) @@ -34,12 +37,13 @@ public PluginConfigurationBuilder(final Project project) this.showSuppressedWarnings = true; this.useCacheFile = true; this.scanScope = ScanScope.getDefaultValue(); - this.projectRelativeFileExclusions = Collections.emptySortedSet(); + this.projectRelativeFileExclusions = null; this.locations = BundledConfig.getAllBundledConfigs() .stream() .map(bc -> configurationLocationFactory(project).create(bc, project)) .collect(Collectors.toCollection(TreeSet::new)); - this.activeLocationIds = Collections.emptySortedSet(); + this.activeLocationIds = null; + this.thirdPartyClasspath = null; this.importSettingsFromMaven = false; } @@ -52,6 +56,7 @@ public PluginConfigurationBuilder(final PluginConfiguration copyFrom) this.projectRelativeFileExclusions = copyFrom.projectRelativeFileExclusions(); this.locations = copyFrom.locations(); this.activeLocationIds = copyFrom.activeLocationIds(); + this.thirdPartyClasspath = copyFrom.thirdPartyClasspath(); this.importSettingsFromMaven = copyFrom.importSettingsFromMaven(); } @@ -87,6 +92,12 @@ public PluginConfigurationBuilder withUseCacheFile(@Nullable final Boolean useCa return this; } + public PluginConfigurationBuilder withScanScope(@NotNull final ScanScope newScanScope) + { + this.scanScope = newScanScope; + return this; + } + public PluginConfigurationBuilder withProjectRelativeFileExclusionsRaw( final Collection projectRelativeFileExclusions) { @@ -119,9 +130,10 @@ public PluginConfigurationBuilder withLocations(@NotNull final SortedSet(), + Objects.requireNonNullElseGet(this.thirdPartyClasspath, ThirdPartyClasspathConfigContainer::createEmpty), this.importSettingsFromMaven, new PluginConfiguration.Cache()); } diff --git a/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java b/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java index b0495db..394e109 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java @@ -1,16 +1,23 @@ package software.xdev.pmd.config; +import java.util.Objects; + import org.jetbrains.annotations.NotNull; import com.intellij.openapi.project.Project; +import software.xdev.pmd.analysis.ProjectScanClasspathManager; import software.xdev.pmd.config.state.project.ProjectConfigurationState; +import software.xdev.pmd.config.state.project.ProjectSettingsState; public class PluginConfigurationManager { private final Project project; + private ProjectSettingsState lastProjectSettingsState; + private PluginConfiguration lastPluginConfiguration; + public PluginConfigurationManager(@NotNull final Project project) { this.project = project; @@ -19,14 +26,38 @@ public PluginConfigurationManager(@NotNull final Project project) @NotNull public PluginConfiguration getCurrent() { - return this.projectConfigurationState() - .populate(new PluginConfigurationBuilder(this.project)) - .build(); + final ProjectConfigurationState projectConfigurationState = this.projectConfigurationState(); + final ProjectSettingsState currentProjectSettingsState = projectConfigurationState.getState(); + if(!Objects.equals(this.lastProjectSettingsState, currentProjectSettingsState)) + { + this.setLastPluginConfiguration( + currentProjectSettingsState, + projectConfigurationState + .populate(new PluginConfigurationBuilder(this.project)) + .build()); + } + + return this.lastPluginConfiguration; } public void setCurrent(@NotNull final PluginConfiguration updatedConfiguration) { - this.projectConfigurationState().setCurrentConfig(updatedConfiguration); + final ProjectConfigurationState projectConfigurationState = this.projectConfigurationState(); + + this.setLastPluginConfiguration(projectConfigurationState.getState(), updatedConfiguration); + + projectConfigurationState.setCurrentConfig(updatedConfiguration); + } + + private void setLastPluginConfiguration( + final ProjectSettingsState projectSettingsState, + @NotNull final PluginConfiguration pluginConfig) + { + this.lastProjectSettingsState = projectSettingsState; + this.lastPluginConfiguration = pluginConfig; + + // Update the classpath information + this.project.getService(ProjectScanClasspathManager.class).configure(pluginConfig.thirdPartyClasspath()); } private ProjectConfigurationState projectConfigurationState() diff --git a/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java b/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java new file mode 100644 index 0000000..256cb40 --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java @@ -0,0 +1,167 @@ +package software.xdev.pmd.config.plugin; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +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.function.Supplier; + +import org.jspecify.annotations.Nullable; + +import com.google.common.base.Suppliers; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.VirtualFile; + +import software.xdev.pmd.util.io.ProjectFilePaths; + + +public record ThirdPartyClasspathConfigContainer( + List classpaths, + List absolutePathStrings, + List urls +) +{ + public static ThirdPartyClasspathConfigContainer createFromUI( + final Project project, + final Collection absolutePathStrings) + { + final Path projectPath = getProjectPath(project); + final ProjectFilePaths projectFilePaths = project.getService(ProjectFilePaths.class); + + return createFromPersisted( + () -> projectPath, + projectFilePaths, + absolutePathStrings.stream() + .filter(Objects::nonNull) + .filter(s -> !s.isBlank()) + .map(s -> { + try + { + return Paths.get(s); + } + catch(final InvalidPathException ipe) + { + return null; + } + }) + .filter(Objects::nonNull) + // Try to relativize + .map(p -> { + if(projectPath == null || !p.startsWith(projectPath)) + { + return p; + } + + try + { + return projectPath.relativize(p); + } + catch(final Exception ex) + { + return p; + } + }) + .map(Path::toString) + .map(projectFilePaths::toUnixPath) + .toList()); + } + + public static ThirdPartyClasspathConfigContainer createFromPersisted( + final Project project, + final Collection classPaths) + { + return createFromPersisted( + Suppliers.memoize(() -> getProjectPath(project)), + project.getService(ProjectFilePaths.class), + classPaths); + } + + static ThirdPartyClasspathConfigContainer createFromPersisted( + final Supplier projectPathSupplier, + final ProjectFilePaths projectFilePaths, + final Collection classPaths) + { + if(classPaths == null || classPaths.isEmpty()) + { + return createEmpty(); + } + + final List validClassPaths = new ArrayList<>(); + final List absolutePathStrings = new ArrayList<>(classPaths.size()); + final List urls = new ArrayList<>(classPaths.size()); + for(final String classpath : classPaths) + { + final Optional optAbsolutePath = Optional.ofNullable(classpath) + .filter(s -> !s.isBlank()) + .map(projectFilePaths::toSystemPath) + .map(Paths::get) + .map(p -> { + if(p.isAbsolute()) + { + return p; + } + + final Path projectPath = projectPathSupplier.get(); + return projectPath != null ? projectPath.resolve(p) : null; + }); + optAbsolutePath + .map(Path::toUri) + .map(uri -> { + try + { + return uri.toURL(); + } + catch(final MalformedURLException ex) + { + return null; + } + }) + .ifPresent(url -> { + validClassPaths.add(classpath); + absolutePathStrings.add(optAbsolutePath.orElseThrow().toString()); + urls.add(url); + }); + } + + return new ThirdPartyClasspathConfigContainer( + Collections.unmodifiableList(validClassPaths), + Collections.unmodifiableList(absolutePathStrings), + Collections.unmodifiableList(urls) + ); + } + + public static ThirdPartyClasspathConfigContainer createEmpty() + { + return new ThirdPartyClasspathConfigContainer(List.of(), List.of(), List.of()); + } + + private static @Nullable Path getProjectPath(final Project project) + { + final VirtualFile projectDir = ProjectUtil.guessProjectDir(project); + return projectDir != null ? projectDir.toNioPath() : null; + } + + @Override + public boolean equals(final Object o) + { + if(!(o instanceof final ThirdPartyClasspathConfigContainer that)) + { + return false; + } + return Objects.equals(this.classpaths, that.classpaths); + } + + @Override + public int hashCode() + { + return Objects.hashCode(this.classpaths); + } +} diff --git a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java index b7e7653..7cc7ee3 100644 --- a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java +++ b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java @@ -3,9 +3,11 @@ import static java.util.Objects.requireNonNullElseGet; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.TreeSet; +import java.util.function.Function; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; @@ -19,9 +21,10 @@ import com.intellij.util.xmlb.annotations.Tag; import com.intellij.util.xmlb.annotations.XCollection; -import software.xdev.pmd.config.PatternContainer; import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; +import software.xdev.pmd.config.plugin.PatternContainer; +import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.config.ConfigurationLocationFactory; import software.xdev.pmd.model.config.ConfigurationType; @@ -50,6 +53,8 @@ public class ProjectSettingsState List activeLocationIds; @MapAnnotation List locations; + @XCollection + List thirdPartyClasspaths; @Tag boolean importSettingsFromMaven; @@ -63,23 +68,40 @@ static ProjectSettingsState create(@NotNull final PluginConfiguration currentCon projectSettings.showSuppressedWarnings = currentConfig.showSuppressedWarnings(); projectSettings.useCacheFile = currentConfig.useCacheFile(); projectSettings.scanScope = currentConfig.scanScope().name(); - projectSettings.projectRelativeFileExclusions = currentConfig.projectRelativeFileExclusions().stream() - .map(PatternContainer::patternString) - .toList(); + projectSettings.projectRelativeFileExclusions = useNullIfEmpty( + currentConfig.projectRelativeFileExclusions(), + l -> l.stream() + .map(PatternContainer::patternString) + .toList()); projectSettings.activeLocationIds = new ArrayList<>(currentConfig.activeLocationIds()); projectSettings.locations = currentConfig.locations().stream() .map(location -> new ConfigurationLocationState( location.getId(), location.getType().name(), - location.getRawLocation(), + location.getLocation(), location.getDescription() )) .toList(); + projectSettings.thirdPartyClasspaths = useNullIfEmpty( + currentConfig.thirdPartyClasspath().classpaths(), + ArrayList::new); projectSettings.importSettingsFromMaven = currentConfig.importSettingsFromMaven(); return projectSettings; } + static , R> R useNullIfEmpty( + final C baseList, + final Function mapperIfPresent) + { + if(baseList.isEmpty()) + { + return null; + } + + return mapperIfPresent.apply(baseList); + } + @SuppressWarnings("unused") public ProjectSettingsState() { @@ -100,6 +122,8 @@ public PluginConfigurationBuilder populate( .withActiveLocationIds(new TreeSet<>(requireNonNullElseGet( this.activeLocationIds, ArrayList::new))) + .withThirdPartyClasspath( + ThirdPartyClasspathConfigContainer.createFromPersisted(project, this.thirdPartyClasspaths)) .withImportSettingFromMaven(this.importSettingsFromMaven); } diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java index 3f1aea3..4b2c818 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java @@ -84,9 +84,9 @@ public final boolean hasChangedFrom(final ConfigurationLocation configurationLoc } @SuppressWarnings("checkstyle:IllegalIdentifierName") - public void validate() throws Exception + public void validate(final ClassLoader classLoader) throws Exception { - final RuleSet ruleSet = this.loadRuleSet(); + final RuleSet ruleSet = this.loadRuleSet(classLoader); if(ruleSet.getRules().isEmpty()) { throw new IllegalStateException("No rules detected"); @@ -94,27 +94,29 @@ public void validate() throws Exception this.cachedRuleSet = ruleSet; } - protected abstract RuleSet loadRuleSet() throws Exception; + protected abstract RuleSet loadRuleSet(final ClassLoader classLoader) throws Exception; - protected abstract boolean shouldReloadRuleSet(); + protected abstract boolean shouldReloadRuleSet(final ClassLoader classLoader); @Nullable - public RuleSet getOrRefreshCachedRuleSet() + public RuleSet getOrRefreshCachedRuleSet(final ClassLoader classLoader) { - if(this.cachedRuleSet == null || this.shouldReloadRuleSet()) + if(this.cachedRuleSet == null || this.shouldReloadRuleSet(classLoader)) { - this.loadRuleSetSyncIfStillRequired(this.cachedRuleSet); + this.loadRuleSetSyncIfStillRequired(this.cachedRuleSet, classLoader); } return this.cachedRuleSet; } - protected synchronized void loadRuleSetSyncIfStillRequired(final RuleSet expectedRuleSetWhenLoadingStarts) + protected synchronized void loadRuleSetSyncIfStillRequired( + final RuleSet expectedRuleSetWhenLoadingStarts, + final ClassLoader classLoader) { if(this.cachedRuleSet == null || this.cachedRuleSet == expectedRuleSetWhenLoadingStarts) { try { - this.cachedRuleSet = this.loadRuleSet(); + this.cachedRuleSet = this.loadRuleSet(classLoader); } catch(final Exception ex) { diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java index b33a8aa..92f166c 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java @@ -8,9 +8,9 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.lang.rule.RuleSet; -import net.sourceforge.pmd.lang.rule.RuleSetLoader; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.model.config.file.pmd.DefaultRuleSetLoaderCreator; public class BundledConfigurationLocation extends ConfigurationLocation @@ -58,20 +58,21 @@ public String getDescription() } @Override - public void validate() + public void validate(final ClassLoader classLoader) { // always valid } @Nullable @Override - protected synchronized RuleSet loadRuleSet() + protected synchronized RuleSet loadRuleSet(final ClassLoader ignored) { - return new RuleSetLoader().loadFromResource(this.getBundledConfig().getLocation()); + return DefaultRuleSetLoaderCreator.createAndLoad(rsl -> rsl + .loadFromResource(this.getBundledConfig().getLocation())); } @Override - protected boolean shouldReloadRuleSet() + protected boolean shouldReloadRuleSet(final ClassLoader ignored) { return false; } diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java index 44cdce3..3774df8 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java @@ -24,13 +24,13 @@ public boolean isRemovable() } @Override - protected synchronized RuleSet loadRuleSet() + protected synchronized RuleSet loadRuleSet(final ClassLoader ignored) { return null; } @Override - public @Nullable RuleSet getOrRefreshCachedRuleSet() + public @Nullable RuleSet getOrRefreshCachedRuleSet(final ClassLoader ignored) { return null; } diff --git a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java index eeaef21..a26187f 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java @@ -1,6 +1,7 @@ package software.xdev.pmd.model.config.file; import java.io.IOException; +import java.lang.ref.WeakReference; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -12,9 +13,10 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.lang.rule.RuleSet; -import net.sourceforge.pmd.lang.rule.RuleSetLoader; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.model.config.file.pmd.DefaultRuleSetLoaderCreator; +import software.xdev.pmd.model.config.file.pmd.LoadFromStringRuleSetLoaderWorkaround; import software.xdev.pmd.util.io.ProjectFilePaths; @@ -24,6 +26,9 @@ public class FileConfigurationLocation extends ConfigurationLocation { protected long nextReloadRuleSetMs; + // Use WeakReference to prevent memory leak + protected WeakReference previouslyUsedClassLoaderRef; + protected Instant lastModifiedFileTime; protected String location; protected Path locationPath; @@ -94,22 +99,29 @@ protected Instant lastModifiedTimeFromLocation() @SuppressWarnings("checkstyle:IllegalIdentifierName") @Override - protected synchronized RuleSet loadRuleSet() throws IOException + protected synchronized RuleSet loadRuleSet(final ClassLoader classLoader) throws IOException { this.nextReloadRuleSetMs = System.currentTimeMillis() + 10 * 1000; - final RuleSet ruleSet = new RuleSetLoader().loadFromString( - this.getLocation(), - new String(Files.readAllBytes(this.locationPath))); + // Do this here due to IOEx + final String rulesetXmlContent = new String(Files.readAllBytes(this.locationPath)); + final RuleSet ruleSet = DefaultRuleSetLoaderCreator.createAndLoad(rsl -> + LoadFromStringRuleSetLoaderWorkaround.loadFromString( + rsl.loadResourcesWith(classLoader), + this.getLocation(), + rulesetXmlContent)); this.lastModifiedFileTime = this.lastModifiedTimeFromLocation(); + this.previouslyUsedClassLoaderRef = new WeakReference<>(classLoader); return ruleSet; } @Override - protected boolean shouldReloadRuleSet() + protected boolean shouldReloadRuleSet(final ClassLoader classLoader) { - // Check if recently checked - return System.currentTimeMillis() > this.nextReloadRuleSetMs + // Check if classloader mismatch + return this.previouslyUsedClassLoaderRef == null || this.previouslyUsedClassLoaderRef.get() != classLoader + // Check if recently checked + || System.currentTimeMillis() > this.nextReloadRuleSetMs // Check if file was modified && (this.lastModifiedFileTime == null || !this.lastModifiedFileTime.equals(this.lastModifiedTimeFromLocation())); diff --git a/src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java b/src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java new file mode 100644 index 0000000..bff69a4 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java @@ -0,0 +1,116 @@ +package software.xdev.pmd.model.config.file.pmd; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.event.Level; + +import com.intellij.openapi.diagnostic.Logger; + +import net.sourceforge.pmd.lang.rule.RuleSet; +import net.sourceforge.pmd.lang.rule.RuleSetLoader; +import net.sourceforge.pmd.util.log.PmdReporter; +import net.sourceforge.pmd.util.log.internal.MessageReporterBase; + + +// Workaround for: https://github.com/pmd/pmd/issues/6912 +public final class DefaultRuleSetLoaderCreator +{ + private static final Logger LOG = Logger.getInstance(DefaultRuleSetLoaderCreator.class); + + private static boolean reflectionInitialized; + private static Method mWithReporter; + + static void initReflection() + { + if(reflectionInitialized) + { + return; + } + reflectionInitialized = true; + + try + { + mWithReporter = RuleSetLoader.class.getDeclaredMethod("withReporter", PmdReporter.class); + mWithReporter.setAccessible(true); + } + catch(final Exception ex) + { + LOG.warn("Failed to get method 'withReporter'", ex); + } + } + + public static RuleSet createAndLoad(final Function loadFunc) + { + initReflection(); + + final RuleSetLoader ruleSetLoader = new RuleSetLoader(); + + final Optional optErrorStoringPmdReporter = Optional.ofNullable(mWithReporter) + .map(m -> { + try + { + final ErrorStoringPmdReporter errorStoringPmdReporter = new ErrorStoringPmdReporter(); + m.invoke(ruleSetLoader, errorStoringPmdReporter); + return errorStoringPmdReporter; + } + catch(final Exception ex) + { + LOG.warn("Failed to invoke 'withReporter'", ex); + return null; + } + }); + + try + { + return loadFunc.apply(ruleSetLoader); + } + catch(final RuntimeException rex) + { + optErrorStoringPmdReporter.ifPresent(r -> r.addErrorsAsSuppressedAndClear(rex)); + throw rex; + } + } + + private DefaultRuleSetLoaderCreator() + { + } + + static class ErrorStoringPmdReporter extends MessageReporterBase implements PmdReporter + { + private final List errors = new ArrayList<>(); + + @Override + protected boolean isLoggableImpl(final Level level) + { + return false; + } + + @Override + public void logEx( + final Level level, + @Nullable final String message, + final Object[] formatArgs, + @Nullable final Throwable error) + { + this.errors.add(error); + super.logEx(level, message, formatArgs, error); + } + + @Override + protected void logImpl(final Level level, final String message) + { + // noop + } + + public void addErrorsAsSuppressedAndClear(final Exception ex) + { + this.errors.forEach(ex::addSuppressed); + this.errors.clear(); + } + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java b/src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java new file mode 100644 index 0000000..a797922 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java @@ -0,0 +1,135 @@ +package software.xdev.pmd.model.config.file.pmd; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import com.intellij.openapi.diagnostic.Logger; + +import net.sourceforge.pmd.lang.rule.RuleSet; +import net.sourceforge.pmd.lang.rule.RuleSetLoader; +import net.sourceforge.pmd.lang.rule.internal.RuleSetReferenceId; +import net.sourceforge.pmd.util.internal.ResourceLoader; + + +// Workaround for: https://github.com/pmd/pmd/issues/6913 +public final class LoadFromStringRuleSetLoaderWorkaround +{ + private static final Logger LOG = Logger.getInstance(LoadFromStringRuleSetLoaderWorkaround.class); + + private static boolean reflectionInitialized; + static boolean reflectionUsable; + + private static Field fResourceLoader; + private static Method mLoadFromResource; + private static Field resourceLoaderFClassLoader; + + static void initReflection() + { + if(reflectionInitialized) + { + return; + } + reflectionInitialized = true; + + try + { + fResourceLoader = RuleSetLoader.class.getDeclaredField("resourceLoader"); + fResourceLoader.setAccessible(true); + } + catch(final Exception ex) + { + LOG.warn("Failed to get field 'resourceLoader'", ex); + } + + try + { + mLoadFromResource = RuleSetLoader.class.getDeclaredMethod( + "loadFromResource", RuleSetReferenceId.class); + mLoadFromResource.setAccessible(true); + } + catch(final Exception ex) + { + LOG.warn("Failed to get method 'loadFromResource'", ex); + } + + try + { + resourceLoaderFClassLoader = ResourceLoader.class.getDeclaredField("classLoader"); + resourceLoaderFClassLoader.setAccessible(true); + } + catch(final Exception ex) + { + LOG.warn("Failed to get method 'classLoader'", ex); + } + + reflectionUsable = fResourceLoader != null + && mLoadFromResource != null + && resourceLoaderFClassLoader != null; + } + + public static RuleSet loadFromString( + final RuleSetLoader ruleSetLoader, + final String filename, + final String rulesetXmlContent) + { + initReflection(); + + if(reflectionUsable) + { + try + { + if(filename == null || filename.isEmpty()) + { + throw new IllegalArgumentException("Invalid empty filename"); + } + + final ResourceLoader oldLoader = (ResourceLoader)fResourceLoader.get(ruleSetLoader); + final ClassLoader oldClassLoader = (ClassLoader)resourceLoaderFClassLoader.get(oldLoader); + + try + { + fResourceLoader.set( + ruleSetLoader, + new ResourceLoader(oldClassLoader) + { + @Override + public @NonNull InputStream loadResourceAsStream(final String name) throws IOException + { + if(Objects.equals(name, filename)) + { + return new ByteArrayInputStream(rulesetXmlContent.getBytes(StandardCharsets.UTF_8)); + } + return oldLoader.loadResourceAsStream(name); + } + }); + return (RuleSet)mLoadFromResource.invoke( + ruleSetLoader, + new RuleSetReferenceId(filename, null)); + } + finally + { + fResourceLoader.set(ruleSetLoader, oldLoader); + } + } + catch(final IllegalAccessException | InvocationTargetException ex) + { + LOG.warn("Failed to invoke loadFromString workaround", ex); + } + } + + // Fallback + return ruleSetLoader.loadFromString(filename, rulesetXmlContent); + } + + private LoadFromStringRuleSetLoaderWorkaround() + { + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index d9bb685..febb108 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -1,6 +1,7 @@ package software.xdev.pmd.ui.config.project; import java.awt.BorderLayout; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -23,13 +24,15 @@ import com.intellij.ui.components.panels.HorizontalLayout; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.config.PatternContainer; import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; +import software.xdev.pmd.config.plugin.PatternContainer; +import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; import software.xdev.pmd.ui.config.project.components.exclusion.FileMaskPanelManager; import software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanelManager; +import software.xdev.pmd.ui.config.project.components.thirdpartyclasspath.ThirdPartyClasspathPanelManager; /** @@ -79,6 +82,7 @@ public PMDConfigPanel(@NotNull final Project project) this.project = project; this.rulefileLocationPanelManager = new LocationPanelManager(project, this); + this.thirdPartyClasspathPanelManager = new ThirdPartyClasspathPanelManager(project, this); this.initialise(); } @@ -117,13 +121,26 @@ private JPanel buildConfigPanel() + "It's recommended to only enable this when importing changed configuration."), this.createDefaultGridBagConstraints(0, 2, 2)); - configFilePanel.add( + this.addPanel( + configFilePanel, this.rulefileLocationPanelManager.buildPanel(), - this.createFullWidthGridBagConstraints(3, 1.0)); + 3, + 1.0, + 300); - configFilePanel.add( + this.addPanel( + configFilePanel, this.exclusionPanelManager.getPanel(), - this.createFullWidthGridBagConstraints(4, 0.1)); + 4, + 0.1, + 150); + + this.addPanel( + configFilePanel, + this.thirdPartyClasspathPanelManager.buildPanel(), + 5, + 0.5, + 150); return configFilePanel; } @@ -172,6 +189,20 @@ private GridBagConstraints createFullWidthGridBagConstraints(final int gridY, fi 0); } + private void addPanel( + final JPanel panel, + final JPanel panelToAdd, + final int gridY, + final double weighty, + final int preferredHeight + ) + { + panelToAdd.setPreferredSize(new Dimension(Integer.MAX_VALUE, preferredHeight)); + panel.add( + panelToAdd, + this.createFullWidthGridBagConstraints(gridY, weighty)); + } + public void showPluginConfiguration(@NotNull final PluginConfiguration pluginConfig) { this.cbScope.setSelectedItem(pluginConfig.scanScope()); @@ -184,6 +215,8 @@ public void showPluginConfiguration(@NotNull final PluginConfiguration pluginCon this.exclusionPanelManager.update(pluginConfig.projectRelativeFileExclusions().stream() .map(PatternContainer::patternString) .collect(Collectors.toCollection(TreeSet::new))); + this.thirdPartyClasspathPanelManager.setThirdPartyClassPath( + pluginConfig.thirdPartyClasspath().absolutePathStrings()); } public PluginConfiguration getPluginConfiguration() @@ -200,6 +233,10 @@ public PluginConfiguration getPluginConfiguration() .withActiveLocationIds(this.rulefileLocationPanelManager.locationModel().getActiveLocations().stream() .map(ConfigurationLocation::getId) .collect(Collectors.toCollection(TreeSet::new))) + .withThirdPartyClasspath( + ThirdPartyClasspathConfigContainer.createFromUI( + this.project, + this.thirdPartyClasspathPanelManager.getThirdPartyClassPath())) .withImportSettingFromMaven(this.chbxImportSettingsFromMaven.isSelected()) .build(); } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java index 34f2a9a..0ea7ecc 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java @@ -23,6 +23,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.util.ui.JBUI; +import software.xdev.pmd.analysis.ProjectScanClasspathManager; import software.xdev.pmd.model.config.ConfigurationLocation; @@ -189,7 +190,8 @@ private Step continueWithValidate(final ConfigurationLocation location) try { - this.configurationLocation.validate(); + this.configurationLocation.validate( + this.project.getService(ProjectScanClasspathManager.class).getClassLoader()); return Step.COMPLETE; } catch(final Exception e) diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java index 8dec1dc..675f2d8 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java @@ -63,7 +63,7 @@ public JPanel buildPanel() container.add(new TitledSeparator("Configuration File"), BorderLayout.NORTH); container.add(tableDecorator.createPanel(), BorderLayout.CENTER); final JLabel infoLabel = new JLabel( - "The active rules file may be overridden, or deactivated, by module settings.", + "The active rules may be overridden or deactivated by module settings", AllIcons.General.Information, SwingConstants.LEFT); infoLabel.setBorder(JBUI.Borders.empty(8, 0, 4, 0)); container.add(infoLabel, BorderLayout.SOUTH); diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java new file mode 100644 index 0000000..3522dbe --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java @@ -0,0 +1,141 @@ +package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; + +import java.awt.BorderLayout; +import java.util.List; +import java.util.stream.IntStream; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; + +import com.intellij.openapi.fileChooser.FileChooser; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.AnActionButtonRunnable; +import com.intellij.ui.TitledSeparator; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.components.JBList; + +import software.xdev.pmd.ui.config.project.PMDConfigPanel; +import software.xdev.pmd.ui.config.project.components.SubPMDConfigPanelManager; + + +public class ThirdPartyClasspathPanelManager extends SubPMDConfigPanelManager +{ + private final DefaultListModel pathListModel = new DefaultListModel<>(); + private final JList pathList = new JBList<>(this.pathListModel); + + public ThirdPartyClasspathPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) + { + super(project, pmdConfigPanel); + } + + public JPanel buildPanel() + { + final JPanel container = new JPanel(new BorderLayout()); + container.add( + new TitledSeparator("Third-Party Rules"), + BorderLayout.NORTH); + container.add(ToolbarDecorator.createDecorator(this.pathList) + .setAddAction(new AddPathAction()) + .setEditAction(new EditPathAction()) + .setRemoveAction(new RemovePathAction()) + .setPreferredSize(DECORATOR_DIMENSIONS) + .createPanel(), + BorderLayout.CENTER); + return container; + } + + public List getThirdPartyClassPath() + { + return IntStream.range(0, this.pathListModel.size()) + .mapToObj(this.pathListModel::get) + .toList(); + } + + public void setThirdPartyClassPath(final List classPath) + { + this.pathListModel.clear(); + this.pathListModel.addAll(classPath); + } + + private static FileChooserDescriptor chooserDescriptor() + { + return new FileChooserDescriptor(true, false, true, true, false, false) + .withFileFilter(file -> { + final String currentExtension = file.getExtension(); + return currentExtension != null && "jar".equalsIgnoreCase(currentExtension.trim()); + }); + } + + class AddPathAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final VirtualFile chosen = FileChooser.chooseFile( + ThirdPartyClasspathPanelManager.chooserDescriptor(), + ThirdPartyClasspathPanelManager.this.pmdConfigPanel, + ThirdPartyClasspathPanelManager.this.project, + ProjectUtil.guessProjectDir(ThirdPartyClasspathPanelManager.this.project)); + if(chosen != null) + { + ThirdPartyClasspathPanelManager.this.pathListModel.addElement( + VfsUtilCore.virtualToIoFile(chosen).getAbsolutePath()); + } + } + } + + + class EditPathAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final int selected = ThirdPartyClasspathPanelManager.this.pathList.getSelectedIndex(); + if(selected < 0) + { + return; + } + + final DefaultListModel listModel = ThirdPartyClasspathPanelManager.this.pathListModel; + final String selectedFile = listModel.get(selected); + + final VirtualFile toSelect = LocalFileSystem.getInstance().findFileByPath(selectedFile); + final VirtualFile chosen = FileChooser.chooseFile( + ThirdPartyClasspathPanelManager.chooserDescriptor(), + ThirdPartyClasspathPanelManager.this.project, + toSelect); + if(chosen != null) + { + listModel.remove(selected); + listModel.add(selected, VfsUtilCore.virtualToIoFile(chosen).getAbsolutePath()); + ThirdPartyClasspathPanelManager.this.pathList.setSelectedIndex(selected); + } + } + } + + + class RemovePathAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final int[] selected = ThirdPartyClasspathPanelManager.this.pathList.getSelectedIndices(); + if(selected == null || selected.length == 0) + { + return; + } + + for(final int index : selected) + { + ThirdPartyClasspathPanelManager.this.pathListModel.remove(index); + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 72a3f86..f397183 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -28,6 +28,7 @@ + diff --git a/src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java b/src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java new file mode 100644 index 0000000..f5e352c --- /dev/null +++ b/src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java @@ -0,0 +1,17 @@ +package software.xdev.pmd.model.config.file.pmd; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +class DefaultRuleSetLoaderCreatorTest +{ + @Test + void checkReflection() + { + Assertions.assertDoesNotThrow(DefaultRuleSetLoaderCreator::initReflection); + + Assertions.assertDoesNotThrow(() -> DefaultRuleSetLoaderCreator.createAndLoad(rsl -> rsl + .loadFromResource("category/java/security.xml"))); + } +} diff --git a/src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java b/src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java new file mode 100644 index 0000000..922a2af --- /dev/null +++ b/src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java @@ -0,0 +1,15 @@ +package software.xdev.pmd.model.config.file.pmd; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +class LoadFromStringRuleSetLoaderWorkaroundTest +{ + @Test + void checkReflection() + { + Assertions.assertDoesNotThrow(LoadFromStringRuleSetLoaderWorkaround::initReflection); + Assertions.assertTrue(LoadFromStringRuleSetLoaderWorkaround.reflectionUsable); + } +} From 158b3dec8185729ec3dfcd1a999a24424e6e3779 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 07:57:39 +0200 Subject: [PATCH 10/30] Move ruleset package --- .../xdev/pmd/analysis/PMDAnalyzer.java | 2 +- .../config/ConfigurationLocationSource.java | 2 +- .../xdev/pmd/config/PluginConfiguration.java | 2 +- .../config/PluginConfigurationBuilder.java | 6 +++--- .../module/ModuleConfigurationState.java | 2 +- .../state/project/ProjectSettingsState.java | 8 ++++---- .../PMDMavenAfterImportConfigurator.java | 8 ++++---- .../ConfigurationLocation.java | 4 ++-- .../ConfigurationLocationFactory.java | 12 ++++++------ .../ConfigurationType.java | 2 +- .../bundled/BundledConfig.java | 4 ++-- .../bundled/BundledConfigurationLocation.java | 8 ++++---- .../bundled/PMDBuiltInRulesFinder.java | 2 +- .../UnknownBundledConfigurationLocation.java | 2 +- .../file/FileConfigurationLocation.java | 10 +++++----- .../RelativeFileConfigurationLocation.java | 4 ++-- .../file/pmd/DefaultRuleSetLoaderCreator.java | 2 +- ...LoadFromStringRuleSetLoaderWorkaround.java | 2 +- .../config/module/PMDModuleConfigPanel.java | 2 +- .../module/PMDModuleConfigurationEditor.java | 2 +- .../pmd/ui/config/project/PMDConfigPanel.java | 2 +- .../rulefilelocation/LocationDialog.java | 2 +- .../rulefilelocation/LocationPanel.java | 19 ++++++------------- .../LocationPanelManager.java | 2 +- .../rulefilelocation/LocationTableModel.java | 2 +- src/main/resources/META-INF/plugin.xml | 2 +- .../pmd/DefaultRuleSetLoaderCreatorTest.java | 2 +- ...FromStringRuleSetLoaderWorkaroundTest.java | 2 +- 28 files changed, 56 insertions(+), 63 deletions(-) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/ConfigurationLocation.java (97%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/ConfigurationLocationFactory.java (82%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/ConfigurationType.java (92%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/bundled/BundledConfig.java (95%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/bundled/BundledConfigurationLocation.java (85%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/bundled/PMDBuiltInRulesFinder.java (93%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/bundled/UnknownBundledConfigurationLocation.java (91%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/FileConfigurationLocation.java (89%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/RelativeFileConfigurationLocation.java (92%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/pmd/DefaultRuleSetLoaderCreator.java (97%) rename src/main/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java (98%) rename src/test/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/pmd/DefaultRuleSetLoaderCreatorTest.java (85%) rename src/test/java/software/xdev/pmd/model/config/{ => rulesetlocation}/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java (83%) diff --git a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java index 3ed9f07..d6ce538 100644 --- a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java +++ b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java @@ -49,7 +49,7 @@ import software.xdev.pmd.config.PluginConfigurationManager; import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; import software.xdev.pmd.langversion.ManagedLanguageVersionResolver; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; public class PMDAnalyzer implements Disposable diff --git a/src/main/java/software/xdev/pmd/config/ConfigurationLocationSource.java b/src/main/java/software/xdev/pmd/config/ConfigurationLocationSource.java index 99427e6..f5fe1e6 100644 --- a/src/main/java/software/xdev/pmd/config/ConfigurationLocationSource.java +++ b/src/main/java/software/xdev/pmd/config/ConfigurationLocationSource.java @@ -14,7 +14,7 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.config.state.module.ModuleConfigurationState; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; public class ConfigurationLocationSource diff --git a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java index 0c9e4a5..0278b6e 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java @@ -13,7 +13,7 @@ import software.xdev.pmd.config.plugin.PatternContainer; import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; diff --git a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java index 0224ff3..2e0018e 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java @@ -14,9 +14,9 @@ import software.xdev.pmd.config.plugin.PatternContainer; import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationLocationFactory; -import software.xdev.pmd.model.config.bundled.BundledConfig; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig; import software.xdev.pmd.model.scope.ScanScope; diff --git a/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java index 2c0496b..a7b06f0 100644 --- a/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java +++ b/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java @@ -21,7 +21,7 @@ import com.intellij.util.xmlb.annotations.XCollection; import software.xdev.pmd.config.PluginConfigurationManager; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; /** diff --git a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java index 7cc7ee3..071ca32 100644 --- a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java +++ b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java @@ -25,10 +25,10 @@ import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.config.plugin.PatternContainer; import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationLocationFactory; -import software.xdev.pmd.model.config.ConfigurationType; -import software.xdev.pmd.model.config.bundled.BundledConfig; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig; import software.xdev.pmd.model.scope.ScanScope; diff --git a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java b/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java index e9e043f..af89ec4 100644 --- a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java +++ b/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java @@ -32,10 +32,10 @@ import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.config.PluginConfigurationManager; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.bundled.BundledConfigurationLocation; -import software.xdev.pmd.model.config.file.FileConfigurationLocation; -import software.xdev.pmd.model.config.file.RelativeFileConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.file.FileConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.file.RelativeFileConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java similarity index 97% rename from src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java index 4b2c818..b449032 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config; +package software.xdev.pmd.model.config.rulesetlocation; import java.util.Comparator; import java.util.Objects; @@ -10,7 +10,7 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.lang.rule.RuleSet; -import software.xdev.pmd.model.config.bundled.BundledConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfigurationLocation; /** diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocationFactory.java similarity index 82% rename from src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocationFactory.java index 019e34e..1006cac 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocationFactory.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config; +package software.xdev.pmd.model.config.rulesetlocation; import java.util.Map; import java.util.Optional; @@ -8,11 +8,11 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; -import software.xdev.pmd.model.config.bundled.BundledConfig; -import software.xdev.pmd.model.config.bundled.BundledConfigurationLocation; -import software.xdev.pmd.model.config.bundled.UnknownBundledConfigurationLocation; -import software.xdev.pmd.model.config.file.FileConfigurationLocation; -import software.xdev.pmd.model.config.file.RelativeFileConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig; +import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.bundled.UnknownBundledConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.file.FileConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.file.RelativeFileConfigurationLocation; public class ConfigurationLocationFactory diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationType.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java similarity index 92% rename from src/main/java/software/xdev/pmd/model/config/ConfigurationType.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java index 7cf8130..effef38 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationType.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config; +package software.xdev.pmd.model.config.rulesetlocation; public enum ConfigurationType { diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfig.java similarity index 95% rename from src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfig.java index c06f060..110404c 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfig.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfig.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.bundled; +package software.xdev.pmd.model.config.rulesetlocation.bundled; import java.util.Arrays; import java.util.Collection; @@ -15,7 +15,7 @@ import com.intellij.openapi.application.ApplicationManager; import software.xdev.pmd.langversion.LanguageVersionResolverService; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; public final class BundledConfig diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfigurationLocation.java similarity index 85% rename from src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfigurationLocation.java index 92f166c..04b547a 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/BundledConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/BundledConfigurationLocation.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.bundled; +package software.xdev.pmd.model.config.rulesetlocation.bundled; import java.util.Objects; @@ -8,9 +8,9 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.lang.rule.RuleSet; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationType; -import software.xdev.pmd.model.config.file.pmd.DefaultRuleSetLoaderCreator; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.file.pmd.DefaultRuleSetLoaderCreator; public class BundledConfigurationLocation extends ConfigurationLocation diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/PMDBuiltInRulesFinder.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/PMDBuiltInRulesFinder.java similarity index 93% rename from src/main/java/software/xdev/pmd/model/config/bundled/PMDBuiltInRulesFinder.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/PMDBuiltInRulesFinder.java index 92d05a6..3fd76dc 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/PMDBuiltInRulesFinder.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/PMDBuiltInRulesFinder.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.bundled; +package software.xdev.pmd.model.config.rulesetlocation.bundled; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/UnknownBundledConfigurationLocation.java similarity index 91% rename from src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/UnknownBundledConfigurationLocation.java index 3774df8..8610a2b 100644 --- a/src/main/java/software/xdev/pmd/model/config/bundled/UnknownBundledConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/bundled/UnknownBundledConfigurationLocation.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.bundled; +package software.xdev.pmd.model.config.rulesetlocation.bundled; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java similarity index 89% rename from src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java index a26187f..86dac58 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.file; +package software.xdev.pmd.model.config.rulesetlocation.file; import java.io.IOException; import java.lang.ref.WeakReference; @@ -13,10 +13,10 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.lang.rule.RuleSet; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationType; -import software.xdev.pmd.model.config.file.pmd.DefaultRuleSetLoaderCreator; -import software.xdev.pmd.model.config.file.pmd.LoadFromStringRuleSetLoaderWorkaround; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.file.pmd.DefaultRuleSetLoaderCreator; +import software.xdev.pmd.model.config.rulesetlocation.file.pmd.LoadFromStringRuleSetLoaderWorkaround; import software.xdev.pmd.util.io.ProjectFilePaths; diff --git a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java similarity index 92% rename from src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java index 536010c..e0a2efc 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java @@ -1,11 +1,11 @@ -package software.xdev.pmd.model.config.file; +package software.xdev.pmd.model.config.rulesetlocation.file; import org.jetbrains.annotations.NotNull; import com.intellij.openapi.components.PathMacroManager; import com.intellij.openapi.project.Project; -import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; /** diff --git a/src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java similarity index 97% rename from src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java index bff69a4..cb64215 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreator.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.file.pmd; +package software.xdev.pmd.model.config.rulesetlocation.file.pmd; import java.lang.reflect.Method; import java.util.ArrayList; diff --git a/src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java similarity index 98% rename from src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java rename to src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java index a797922..9b43778 100644 --- a/src/main/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.file.pmd; +package software.xdev.pmd.model.config.rulesetlocation.file.pmd; import java.io.ByteArrayInputStream; import java.io.IOException; diff --git a/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigPanel.java index 03e16ef..4bb9786 100644 --- a/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigPanel.java @@ -25,7 +25,7 @@ import com.intellij.openapi.ui.ComboBox; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; /** diff --git a/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigurationEditor.java b/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigurationEditor.java index 516e08e..d8abe71 100644 --- a/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigurationEditor.java +++ b/src/main/java/software/xdev/pmd/ui/config/module/PMDModuleConfigurationEditor.java @@ -16,7 +16,7 @@ import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationManager; import software.xdev.pmd.config.state.module.ModuleConfigurationState; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; public class PMDModuleConfigurationEditor implements ModuleConfigurationEditor diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index febb108..7f4b145 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -28,7 +28,7 @@ import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.config.plugin.PatternContainer; import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; import software.xdev.pmd.ui.config.project.components.exclusion.FileMaskPanelManager; import software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanelManager; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java index 0ea7ecc..e0b435f 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java @@ -24,7 +24,7 @@ import com.intellij.util.ui.JBUI; import software.xdev.pmd.analysis.ProjectScanClasspathManager; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; /** diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java index 60fc097..d8711cb 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java @@ -1,7 +1,7 @@ package software.xdev.pmd.ui.config.project.components.rulefilelocation; -import static software.xdev.pmd.model.config.ConfigurationType.LOCAL_FILE; -import static software.xdev.pmd.model.config.ConfigurationType.PROJECT_RELATIVE; +import static software.xdev.pmd.model.config.rulesetlocation.ConfigurationType.LOCAL_FILE; +import static software.xdev.pmd.model.config.rulesetlocation.ConfigurationType.PROJECT_RELATIVE; import static software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanel.LocationType.FILE; import java.awt.GridBagConstraints; @@ -37,9 +37,9 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.model.config.ConfigurationLocation; -import software.xdev.pmd.model.config.ConfigurationLocationFactory; -import software.xdev.pmd.model.config.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; import software.xdev.pmd.util.io.ProjectFilePaths; @@ -66,11 +66,7 @@ public LocationPanel(final Project project) { super(new GridBagLayout()); - if(project == null) - { - throw new IllegalArgumentException("Project may not be null"); - } - this.project = project; + this.project = Objects.requireNonNull(project); this.initialise(); } @@ -279,9 +275,6 @@ public void actionPerformed(final ActionEvent e) } - /** - * Handles radio button selections. - */ private final class RadioButtonActionListener implements ActionListener { @Override diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java index 675f2d8..80a3eb8 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java @@ -24,7 +24,7 @@ import com.intellij.ui.table.JBTable; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.ui.config.project.PMDConfigPanel; import software.xdev.pmd.ui.config.project.components.SubPMDConfigPanelManager; diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java index cc77779..e6b42f2 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; /** diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f397183..57240ee 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -25,7 +25,7 @@ - + diff --git a/src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java similarity index 85% rename from src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java rename to src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java index f5e352c..7426e0c 100644 --- a/src/test/java/software/xdev/pmd/model/config/file/pmd/DefaultRuleSetLoaderCreatorTest.java +++ b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.file.pmd; +package software.xdev.pmd.model.config.rulesetlocation.file.pmd; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java similarity index 83% rename from src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java rename to src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java index 922a2af..f95a21a 100644 --- a/src/test/java/software/xdev/pmd/model/config/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java +++ b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.file.pmd; +package software.xdev.pmd.model.config.rulesetlocation.file.pmd; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; From 67a53608ccd240d9f3a7fbbbb0f739e4d4af783c Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 07:59:21 +0200 Subject: [PATCH 11/30] Remove not needed conversion logic --- .../model/config/rulesetlocation/ConfigurationType.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java index effef38..90bbca5 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java @@ -28,12 +28,6 @@ public static ConfigurationType parse(final String typeAsString) return null; } - final String processedType = typeAsString.toUpperCase().replace(' ', '_'); - if("FILE".equals(processedType)) - { - return LOCAL_FILE; - } - - return valueOf(processedType); + return valueOf(typeAsString); } } From 5bf3b8dc5ada0905493f9125e443c8900e8107ee Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 08:07:28 +0200 Subject: [PATCH 12/30] Improve sorting --- .../rulesetlocation/ConfigurationLocation.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java index b449032..df25de6 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java @@ -185,22 +185,18 @@ public int compareTo(@NotNull final ConfigurationLocation other) private int compareStrings(@Nullable final String pStr1, @Nullable final String pStr2) { - int result = 0; if(pStr1 != null) { if(pStr2 != null) { - result = pStr1.compareTo(pStr2); - } - else - { - result = -1; + return pStr1.compareTo(pStr2); } + return -1; } else if(pStr2 != null) { - result = 1; + return 1; } - return result; + return 0; } } From 62d1123f66c2380b1f7d01f33e016218ae803471 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 08:16:22 +0200 Subject: [PATCH 13/30] Fix CS --- .../xdev/pmd/config/state/project/ProjectSettingsState.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java index 071ca32..78f7ae0 100644 --- a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java +++ b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java @@ -91,15 +91,15 @@ static ProjectSettingsState create(@NotNull final PluginConfiguration currentCon } static , R> R useNullIfEmpty( - final C baseList, + final C inputs, final Function mapperIfPresent) { - if(baseList.isEmpty()) + if(inputs.isEmpty()) { return null; } - return mapperIfPresent.apply(baseList); + return mapperIfPresent.apply(inputs); } @SuppressWarnings("unused") From 3e3b1925786513f8d8a27053c5b740b134a65347 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 08:40:15 +0200 Subject: [PATCH 14/30] Configuration of default project is not supported --- .../rulesetlocation/ConfigurationLocation.java | 5 ----- .../file/RelativeFileConfigurationLocation.java | 6 ------ .../rulefilelocation/LocationDialog.java | 14 +------------- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java index df25de6..59af910 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationLocation.java @@ -43,11 +43,6 @@ protected ConfigurationLocation( this.logger = Logger.getInstance(this.getClass()); } - public boolean canBeResolvedInDefaultProject() - { - return true; - } - protected final Project getProject() { return this.project; diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java index e0a2efc..e4cc727 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java @@ -23,12 +23,6 @@ public RelativeFileConfigurationLocation( super(project, id, ConfigurationType.PROJECT_RELATIVE); } - @Override - public boolean canBeResolvedInDefaultProject() - { - return false; - } - @SuppressWarnings("checkstyle:FinalParameters") @Override public void setLocation(String location) diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java index e0b435f..9d9531a 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java @@ -201,12 +201,6 @@ private Step continueWithValidate(final ConfigurationLocation location) } } - private Step continueWithoutValidate(final ConfigurationLocation location) - { - this.configurationLocation = location; - return Step.COMPLETE; - } - void onPrevious() { this.previousButton.setEnabled(false); @@ -253,13 +247,7 @@ void onNext() return; } - if(!this.project.isDefault() || location.canBeResolvedInDefaultProject()) - { - this.moveToStep(this.continueWithValidate(location)); - return; - } - - this.moveToStep(this.continueWithoutValidate(location)); + this.moveToStep(this.continueWithValidate(location)); return; case COMPLETE: From 9d61836bdf07f9b1e33efde464806bc345cf8409 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 08:54:19 +0200 Subject: [PATCH 15/30] Fix doc --- .../pmd/model/config/rulesetlocation/ConfigurationType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java index 90bbca5..dc2cfe4 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java @@ -4,7 +4,7 @@ public enum ConfigurationType { /** * one of the configurations bundled with the Checkstyle tool, chosen from the - * {@link software.xdev.pmd.csapi.BundledConfig} enum + * {@link software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig} enum */ BUNDLED, From e78c65841a09ea4a1760cc79a62e384a4421bdfc Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 30 Jul 2026 17:12:11 +0200 Subject: [PATCH 16/30] 3rd party rules v2 rough implementation --- .../analysis/ProjectScanClasspathManager.java | 17 +- .../xdev/pmd/config/PluginConfiguration.java | 5 +- .../config/PluginConfigurationBuilder.java | 16 +- .../config/PluginConfigurationManager.java | 2 +- .../ThirdPartyClasspathConfigContainer.java | 167 ----------- .../state/project/ProjectSettingsState.java | 21 +- .../state/project/ThirdPartyCPState.java | 160 +++++++++++ .../FileThirdPartyCPLocationState.java | 27 ++ .../MavenThirdPartyCPLocationState.java | 48 ++++ .../ThirdPartyCPLocationState.java | 25 ++ .../ThirdPartyCPLocation.java | 52 ++++ .../ThirdPartyCPLocationFactory.java | 36 +++ .../ThirdPartyCPLocationType.java | 8 + .../file/FileThirdPartyCPLocation.java | 63 +++++ .../file/FileThirdPartyCPLocationFactory.java | 40 +++ .../AbsoluteFileThirdPartyCPLocation.java | 19 ++ ...soluteFileThirdPartyCPLocationFactory.java | 44 +++ .../RelativeFileThirdPartyCPLocation.java | 19 ++ ...lativeFileThirdPartyCPLocationFactory.java | 63 +++++ .../thirdpartycplocation/maven/MavenId.java | 51 ++++ .../maven/MavenThirdPartyCPLocation.java | 54 ++++ .../MavenThirdPartyCPLocationFactory.java | 51 ++++ .../pmd/ui/config/project/PMDConfigPanel.java | 33 +-- .../LocationPanelManager.java | 159 ----------- .../rulesetlocation/RSErrorPanel.java | 19 ++ .../rulesetlocation/RSLocationDialog.java | 57 ++++ .../RSLocationPanel.java} | 48 ++-- .../RSLocationPanelManager.java | 81 ++++++ .../RSLocationTableModel.java} | 34 +-- .../CompletePanel.java | 4 +- .../ErrorPanel.java | 14 +- .../LocationDialog.java | 118 ++++---- .../shared/LocationPanelManager.java | 120 ++++++++ .../components/shared/LocationTableModel.java | 51 ++++ .../TPCPLocationDialog.java | 28 ++ .../TPCPLocationPanel.java | 265 ++++++++++++++++++ .../TPCPLocationPanelManager.java | 51 ++++ .../TPCPLocationTableModel.java | 65 +++++ .../ThirdPartyClasspathPanelManager.java | 141 ---------- .../software/xdev/pmd/util/io/FilePaths.java | 135 --------- .../xdev/pmd/util/io/ProjectFilePaths.java | 6 +- src/main/resources/META-INF/plugin.xml | 6 + 42 files changed, 1634 insertions(+), 789 deletions(-) delete mode 100644 src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java create mode 100644 src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java create mode 100644 src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/FileThirdPartyCPLocationState.java create mode 100644 src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/MavenThirdPartyCPLocationState.java create mode 100644 src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/ThirdPartyCPLocationState.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocation.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationFactory.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationType.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocation.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocationFactory.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocation.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocationFactory.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocation.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocationFactory.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java create mode 100644 src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java delete mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSErrorPanel.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationDialog.java rename src/main/java/software/xdev/pmd/ui/config/project/components/{rulefilelocation/LocationPanel.java => rulesetlocation/RSLocationPanel.java} (87%) create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanelManager.java rename src/main/java/software/xdev/pmd/ui/config/project/components/{rulefilelocation/LocationTableModel.java => rulesetlocation/RSLocationTableModel.java} (82%) rename src/main/java/software/xdev/pmd/ui/config/project/components/{rulefilelocation => shared}/CompletePanel.java (77%) rename src/main/java/software/xdev/pmd/ui/config/project/components/{rulefilelocation => shared}/ErrorPanel.java (76%) rename src/main/java/software/xdev/pmd/ui/config/project/components/{rulefilelocation => shared}/LocationDialog.java (68%) create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationPanelManager.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationTableModel.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationDialog.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java delete mode 100644 src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java delete mode 100644 src/main/java/software/xdev/pmd/util/io/FilePaths.java diff --git a/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java b/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java index e3219e7..28f9301 100644 --- a/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java +++ b/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java @@ -3,12 +3,13 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.List; import java.util.Objects; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; -import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; public class ProjectScanClasspathManager implements Disposable @@ -16,20 +17,22 @@ public class ProjectScanClasspathManager implements Disposable private static final ClassLoader DEFAULT_CL = ProjectScanClasspathManager.class.getClassLoader(); private static final Logger LOG = Logger.getInstance(ProjectScanClasspathManager.class); - private ThirdPartyClasspathConfigContainer cachedConfigContainerKey; + private List cachedKey; private URLClassLoader classLoader; - public void configure(final ThirdPartyClasspathConfigContainer configContainer) + public void configure(final List locations) { - if(!Objects.equals(this.cachedConfigContainerKey, configContainer)) + if(!Objects.equals(this.cachedKey, locations)) { this.closeCurrentClassLoader(); - if(!configContainer.urls().isEmpty()) + if(!locations.isEmpty()) { - this.classLoader = new URLClassLoader(configContainer.urls().toArray(URL[]::new), DEFAULT_CL); + this.classLoader = new URLClassLoader( + locations.stream().map(ThirdPartyCPLocation::url).toArray(URL[]::new), + DEFAULT_CL); } - this.cachedConfigContainerKey = configContainer; + this.cachedKey = locations; } } diff --git a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java index 0278b6e..b5cbc0b 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfiguration.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfiguration.java @@ -1,6 +1,7 @@ package software.xdev.pmd.config; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.SortedSet; @@ -12,8 +13,8 @@ import org.jetbrains.annotations.Nullable; import software.xdev.pmd.config.plugin.PatternContainer; -import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; import software.xdev.pmd.model.scope.ScanScope; @@ -29,7 +30,7 @@ public record PluginConfiguration( SortedSet projectRelativeFileExclusions, SortedSet locations, SortedSet activeLocationIds, - ThirdPartyClasspathConfigContainer thirdPartyClasspath, + List thirdPartyCPLocations, boolean importSettingsFromMaven, Cache cache ) diff --git a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java index 2e0018e..9142fd7 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfigurationBuilder.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -13,10 +14,10 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.config.plugin.PatternContainer; -import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory; import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; import software.xdev.pmd.model.scope.ScanScope; @@ -29,7 +30,7 @@ public final class PluginConfigurationBuilder private SortedSet projectRelativeFileExclusions; private SortedSet locations; private SortedSet activeLocationIds; - private ThirdPartyClasspathConfigContainer thirdPartyClasspath; + private List thirdPartyCPLocations; private boolean importSettingsFromMaven; public PluginConfigurationBuilder(final Project project) @@ -43,7 +44,7 @@ public PluginConfigurationBuilder(final Project project) .map(bc -> configurationLocationFactory(project).create(bc, project)) .collect(Collectors.toCollection(TreeSet::new)); this.activeLocationIds = null; - this.thirdPartyClasspath = null; + this.thirdPartyCPLocations = null; this.importSettingsFromMaven = false; } @@ -56,7 +57,7 @@ public PluginConfigurationBuilder(final PluginConfiguration copyFrom) this.projectRelativeFileExclusions = copyFrom.projectRelativeFileExclusions(); this.locations = copyFrom.locations(); this.activeLocationIds = copyFrom.activeLocationIds(); - this.thirdPartyClasspath = copyFrom.thirdPartyClasspath(); + this.thirdPartyCPLocations = copyFrom.thirdPartyCPLocations(); this.importSettingsFromMaven = copyFrom.importSettingsFromMaven(); } @@ -130,10 +131,9 @@ public PluginConfigurationBuilder withLocations(@NotNull final SortedSet thirdPartyCPLocations) { - this.thirdPartyClasspath = thirdPartyClasspath; + this.thirdPartyCPLocations = thirdPartyCPLocations; return this; } @@ -159,7 +159,7 @@ public PluginConfiguration build() .filter(Objects::nonNull) .collect(Collectors.toCollection(TreeSet::new)) : new TreeSet<>(), - Objects.requireNonNullElseGet(this.thirdPartyClasspath, ThirdPartyClasspathConfigContainer::createEmpty), + Collections.unmodifiableList(Objects.requireNonNullElseGet(this.thirdPartyCPLocations, List::of)), this.importSettingsFromMaven, new PluginConfiguration.Cache()); } diff --git a/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java b/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java index 394e109..967ff34 100644 --- a/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java +++ b/src/main/java/software/xdev/pmd/config/PluginConfigurationManager.java @@ -57,7 +57,7 @@ private void setLastPluginConfiguration( this.lastPluginConfiguration = pluginConfig; // Update the classpath information - this.project.getService(ProjectScanClasspathManager.class).configure(pluginConfig.thirdPartyClasspath()); + this.project.getService(ProjectScanClasspathManager.class).configure(pluginConfig.thirdPartyCPLocations()); } private ProjectConfigurationState projectConfigurationState() diff --git a/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java b/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java deleted file mode 100644 index 256cb40..0000000 --- a/src/main/java/software/xdev/pmd/config/plugin/ThirdPartyClasspathConfigContainer.java +++ /dev/null @@ -1,167 +0,0 @@ -package software.xdev.pmd.config.plugin; - -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -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.function.Supplier; - -import org.jspecify.annotations.Nullable; - -import com.google.common.base.Suppliers; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectUtil; -import com.intellij.openapi.vfs.VirtualFile; - -import software.xdev.pmd.util.io.ProjectFilePaths; - - -public record ThirdPartyClasspathConfigContainer( - List classpaths, - List absolutePathStrings, - List urls -) -{ - public static ThirdPartyClasspathConfigContainer createFromUI( - final Project project, - final Collection absolutePathStrings) - { - final Path projectPath = getProjectPath(project); - final ProjectFilePaths projectFilePaths = project.getService(ProjectFilePaths.class); - - return createFromPersisted( - () -> projectPath, - projectFilePaths, - absolutePathStrings.stream() - .filter(Objects::nonNull) - .filter(s -> !s.isBlank()) - .map(s -> { - try - { - return Paths.get(s); - } - catch(final InvalidPathException ipe) - { - return null; - } - }) - .filter(Objects::nonNull) - // Try to relativize - .map(p -> { - if(projectPath == null || !p.startsWith(projectPath)) - { - return p; - } - - try - { - return projectPath.relativize(p); - } - catch(final Exception ex) - { - return p; - } - }) - .map(Path::toString) - .map(projectFilePaths::toUnixPath) - .toList()); - } - - public static ThirdPartyClasspathConfigContainer createFromPersisted( - final Project project, - final Collection classPaths) - { - return createFromPersisted( - Suppliers.memoize(() -> getProjectPath(project)), - project.getService(ProjectFilePaths.class), - classPaths); - } - - static ThirdPartyClasspathConfigContainer createFromPersisted( - final Supplier projectPathSupplier, - final ProjectFilePaths projectFilePaths, - final Collection classPaths) - { - if(classPaths == null || classPaths.isEmpty()) - { - return createEmpty(); - } - - final List validClassPaths = new ArrayList<>(); - final List absolutePathStrings = new ArrayList<>(classPaths.size()); - final List urls = new ArrayList<>(classPaths.size()); - for(final String classpath : classPaths) - { - final Optional optAbsolutePath = Optional.ofNullable(classpath) - .filter(s -> !s.isBlank()) - .map(projectFilePaths::toSystemPath) - .map(Paths::get) - .map(p -> { - if(p.isAbsolute()) - { - return p; - } - - final Path projectPath = projectPathSupplier.get(); - return projectPath != null ? projectPath.resolve(p) : null; - }); - optAbsolutePath - .map(Path::toUri) - .map(uri -> { - try - { - return uri.toURL(); - } - catch(final MalformedURLException ex) - { - return null; - } - }) - .ifPresent(url -> { - validClassPaths.add(classpath); - absolutePathStrings.add(optAbsolutePath.orElseThrow().toString()); - urls.add(url); - }); - } - - return new ThirdPartyClasspathConfigContainer( - Collections.unmodifiableList(validClassPaths), - Collections.unmodifiableList(absolutePathStrings), - Collections.unmodifiableList(urls) - ); - } - - public static ThirdPartyClasspathConfigContainer createEmpty() - { - return new ThirdPartyClasspathConfigContainer(List.of(), List.of(), List.of()); - } - - private static @Nullable Path getProjectPath(final Project project) - { - final VirtualFile projectDir = ProjectUtil.guessProjectDir(project); - return projectDir != null ? projectDir.toNioPath() : null; - } - - @Override - public boolean equals(final Object o) - { - if(!(o instanceof final ThirdPartyClasspathConfigContainer that)) - { - return false; - } - return Objects.equals(this.classpaths, that.classpaths); - } - - @Override - public int hashCode() - { - return Objects.hashCode(this.classpaths); - } -} diff --git a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java index 78f7ae0..4ff3fc3 100644 --- a/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java +++ b/src/main/java/software/xdev/pmd/config/state/project/ProjectSettingsState.java @@ -24,7 +24,6 @@ import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.config.plugin.PatternContainer; -import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; @@ -53,8 +52,8 @@ public class ProjectSettingsState List activeLocationIds; @MapAnnotation List locations; - @XCollection - List thirdPartyClasspaths; + @MapAnnotation + ThirdPartyCPState thirdPartyCPState; @Tag boolean importSettingsFromMaven; @@ -82,15 +81,18 @@ static ProjectSettingsState create(@NotNull final PluginConfiguration currentCon location.getDescription() )) .toList(); - projectSettings.thirdPartyClasspaths = useNullIfEmpty( - currentConfig.thirdPartyClasspath().classpaths(), - ArrayList::new); + projectSettings.thirdPartyCPState = ThirdPartyCPState.create(currentConfig.thirdPartyCPLocations()); projectSettings.importSettingsFromMaven = currentConfig.importSettingsFromMaven(); return projectSettings; } - static , R> R useNullIfEmpty( + public static > C useNullIfEmpty(final C inputs) + { + return inputs.isEmpty() ? null : inputs; + } + + public static , R> R useNullIfEmpty( final C inputs, final Function mapperIfPresent) { @@ -122,8 +124,9 @@ public PluginConfigurationBuilder populate( .withActiveLocationIds(new TreeSet<>(requireNonNullElseGet( this.activeLocationIds, ArrayList::new))) - .withThirdPartyClasspath( - ThirdPartyClasspathConfigContainer.createFromPersisted(project, this.thirdPartyClasspaths)) + .withThirdPartyCPLocations(this.thirdPartyCPState != null + ? this.thirdPartyCPState.populate(project) + : null) .withImportSettingFromMaven(this.importSettingsFromMaven); } diff --git a/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java b/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java new file mode 100644 index 0000000..0621daa --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java @@ -0,0 +1,160 @@ +package software.xdev.pmd.config.state.project; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.util.xmlb.annotations.MapAnnotation; +import com.intellij.util.xmlb.annotations.XCollection; + +import software.xdev.pmd.config.state.project.thirdpartycp.FileThirdPartyCPLocationState; +import software.xdev.pmd.config.state.project.thirdpartycp.MavenThirdPartyCPLocationState; +import software.xdev.pmd.config.state.project.thirdpartycp.ThirdPartyCPLocationState; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationFactory; +import software.xdev.pmd.model.config.thirdpartycplocation.file.absolute.AbsoluteFileThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.file.absolute.AbsoluteFileThirdPartyCPLocationFactory; +import software.xdev.pmd.model.config.thirdpartycplocation.file.relative.RelativeFileThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.file.relative.RelativeFileThirdPartyCPLocationFactory; +import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenThirdPartyCPLocationFactory; + + +// NOTE: Must be located in same package or plugin state import will fail during boot! +public class ThirdPartyCPState +{ + private static final Logger LOG = Logger.getInstance(ThirdPartyCPState.class); + + @XCollection + LinkedHashSet activeIds; + + @MapAnnotation + List maven; + + @MapAnnotation + List absoluteFile; + + @MapAnnotation + List relativeFile; + + @SuppressWarnings("unused") + public ThirdPartyCPState() + { + // for serialization + } + + public static ThirdPartyCPState create( + final List thirdPartyCPLocations) + { + if(thirdPartyCPLocations.isEmpty()) + { + return null; + } + + final ThirdPartyCPState state = new ThirdPartyCPState(); + state.activeIds = thirdPartyCPLocations.stream() + .map(ThirdPartyCPLocation::id) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + final Map, List> groupedByClazz = + thirdPartyCPLocations.stream().collect(Collectors.groupingBy(ThirdPartyCPLocation::getClass)); + + state.maven = create( + groupedByClazz, + MavenThirdPartyCPLocation.class, + loc -> new MavenThirdPartyCPLocationState( + loc.id(), + loc.mavenId().groupId(), + loc.mavenId().artifactId(), + loc.mavenId().version())); + + state.absoluteFile = create( + groupedByClazz, + AbsoluteFileThirdPartyCPLocation.class, + loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())); + + state.relativeFile = create( + groupedByClazz, + RelativeFileThirdPartyCPLocation.class, + loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())); + + return state; + } + + @Nullable + private static < + L extends ThirdPartyCPLocation, + S extends ThirdPartyCPLocationState> + List create( + final Map, List> groupedByClazz, + final Class clazz, + final Function toState) + { + final List thirdPartyCPLocations = groupedByClazz.get(clazz); + if(thirdPartyCPLocations == null || thirdPartyCPLocations.isEmpty()) + { + return null; + } + return thirdPartyCPLocations + .stream() + .map(clazz::cast) + .map(toState) + .filter(Objects::nonNull) + .toList(); + } + + public List populate(@NotNull final Project project) + { + final Map availableLocations = Stream.of( + this.populateLocations(MavenThirdPartyCPLocationFactory.class, project, this.maven), + this.populateLocations(AbsoluteFileThirdPartyCPLocationFactory.class, project, this.absoluteFile), + this.populateLocations(RelativeFileThirdPartyCPLocationFactory.class, project, this.relativeFile)) + .flatMap(List::stream) + .collect(Collectors.toMap(ThirdPartyCPLocation::id, Function.identity())); + + return this.activeIds.stream() + .map(availableLocations::get) + .toList(); + } + + private < + L extends ThirdPartyCPLocation, + S extends ThirdPartyCPLocationState, + F extends ThirdPartyCPLocationFactory> + List populateLocations( + final Class factoryClazz, + final Project project, + final List persistedStates + ) + { + if(persistedStates == null || persistedStates.isEmpty()) + { + return List.of(); + } + final F factory = project.getService(factoryClazz); + return persistedStates.stream() + .filter(s -> this.activeIds.contains(s.id())) + .map(s -> { + try + { + return (ThirdPartyCPLocation)factory.fromPersisted(s); + } + catch(final Exception ex) + { + LOG.error("Encountered problem while populating location[id=" + s.id() + "]", ex); + return null; + } + }) + .filter(Objects::nonNull) + .toList(); + } +} diff --git a/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/FileThirdPartyCPLocationState.java b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/FileThirdPartyCPLocationState.java new file mode 100644 index 0000000..c0cef3e --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/FileThirdPartyCPLocationState.java @@ -0,0 +1,27 @@ +package software.xdev.pmd.config.state.project.thirdpartycp; + +import com.intellij.util.xmlb.annotations.Text; + + +public class FileThirdPartyCPLocationState extends ThirdPartyCPLocationState +{ + @Text + protected String location; + + @SuppressWarnings("unused") + public FileThirdPartyCPLocationState() + { + // for serialisation + } + + public FileThirdPartyCPLocationState(final String id, final String location) + { + super(id); + this.location = location; + } + + public String location() + { + return this.location; + } +} diff --git a/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/MavenThirdPartyCPLocationState.java b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/MavenThirdPartyCPLocationState.java new file mode 100644 index 0000000..ddedc8c --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/MavenThirdPartyCPLocationState.java @@ -0,0 +1,48 @@ +package software.xdev.pmd.config.state.project.thirdpartycp; + +import com.intellij.util.xmlb.annotations.Attribute; + + +public class MavenThirdPartyCPLocationState extends ThirdPartyCPLocationState +{ + @Attribute + protected String groupId; + @Attribute + protected String artifactId; + @Attribute + protected String version; + + @SuppressWarnings("unused") + public MavenThirdPartyCPLocationState() + { + // for serialization + } + + + public MavenThirdPartyCPLocationState( + final String id, + final String groupId, + final String artifactId, + final String version) + { + super(id); + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + public String groupId() + { + return this.groupId; + } + + public String artifactId() + { + return this.artifactId; + } + + public String version() + { + return this.version; + } +} diff --git a/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/ThirdPartyCPLocationState.java b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/ThirdPartyCPLocationState.java new file mode 100644 index 0000000..6f22041 --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/project/thirdpartycp/ThirdPartyCPLocationState.java @@ -0,0 +1,25 @@ +package software.xdev.pmd.config.state.project.thirdpartycp; + +import com.intellij.util.xmlb.annotations.Attribute; + + +public abstract class ThirdPartyCPLocationState +{ + @Attribute + protected String id; + + protected ThirdPartyCPLocationState() + { + // for serialization + } + + protected ThirdPartyCPLocationState(final String id) + { + this.id = id; + } + + public String id() + { + return this.id; + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocation.java new file mode 100644 index 0000000..6d761a3 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocation.java @@ -0,0 +1,52 @@ +package software.xdev.pmd.model.config.thirdpartycplocation; + +import java.net.URL; +import java.util.Objects; + + +public abstract class ThirdPartyCPLocation +{ + private final ThirdPartyCPLocationType type; + private final String id; + private final URL url; + + protected ThirdPartyCPLocation(final ThirdPartyCPLocationType type, final String id, final URL url) + { + this.type = Objects.requireNonNull(type); + this.id = Objects.requireNonNull(id); + this.url = Objects.requireNonNull(url); + } + + public ThirdPartyCPLocationType type() + { + return this.type; + } + + public String id() + { + return this.id; + } + + public URL url() + { + return this.url; + } + + public abstract String displayLocation(); + + @Override + public boolean equals(final Object o) + { + if(!(o instanceof final ThirdPartyCPLocation that)) + { + return false; + } + return this.type == that.type; + } + + @Override + public int hashCode() + { + return Objects.hashCode(this.type); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationFactory.java new file mode 100644 index 0000000..850e485 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationFactory.java @@ -0,0 +1,36 @@ +package software.xdev.pmd.model.config.thirdpartycplocation; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.util.UUID; + +import software.xdev.pmd.config.state.project.thirdpartycp.ThirdPartyCPLocationState; + + +public abstract class ThirdPartyCPLocationFactory< + L extends ThirdPartyCPLocation, + S extends ThirdPartyCPLocationState, + U> +{ + public abstract L fromPersisted(S state); + + public abstract L fromUI(U uiState); + + public URL pathToUrl(final Path path) + { + try + { + return path.toUri().toURL(); + } + catch(final MalformedURLException e) + { + throw new IllegalArgumentException("Path " + path + " can't be converted to url", e); + } + } + + protected String newRandomId() + { + return UUID.randomUUID().toString(); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationType.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationType.java new file mode 100644 index 0000000..b6cbb39 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/ThirdPartyCPLocationType.java @@ -0,0 +1,8 @@ +package software.xdev.pmd.model.config.thirdpartycplocation; + +public enum ThirdPartyCPLocationType +{ + ABSOLUTE_FILE, + RELATIVE_FILE, + MAVEN_ARTIFACT +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocation.java new file mode 100644 index 0000000..b839cd8 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocation.java @@ -0,0 +1,63 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file; + +import java.net.URL; +import java.util.Objects; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationType; + + +public abstract class FileThirdPartyCPLocation extends ThirdPartyCPLocation +{ + private final String location; // Used for storage + private final String absolutePath; // Used for UI (file-picker) + + protected FileThirdPartyCPLocation( + final ThirdPartyCPLocationType type, + final String id, + final URL url, + final String location, + final String absolutePath) + { + super(type, id, url); + this.location = Objects.requireNonNull(location); + this.absolutePath = Objects.requireNonNull(absolutePath); + } + + public String location() + { + return this.location; + } + + public String absolutePath() + { + return this.absolutePath; + } + + @Override + public String displayLocation() + { + return this.location(); + } + + @Override + public boolean equals(final Object o) + { + if(o == null || this.getClass() != o.getClass()) + { + return false; + } + if(!super.equals(o)) + { + return false; + } + final FileThirdPartyCPLocation that = (FileThirdPartyCPLocation)o; + return Objects.equals(this.location, that.location); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), this.location); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocationFactory.java new file mode 100644 index 0000000..fab4a6f --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/FileThirdPartyCPLocationFactory.java @@ -0,0 +1,40 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.config.state.project.thirdpartycp.FileThirdPartyCPLocationState; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationFactory; +import software.xdev.pmd.util.io.ProjectFilePaths; + + +public abstract class FileThirdPartyCPLocationFactory + extends ThirdPartyCPLocationFactory +{ + protected final ProjectFilePaths projectFilePaths; + + protected FileThirdPartyCPLocationFactory(final Project project) + { + this.projectFilePaths = project.getService(ProjectFilePaths.class); + } + + protected Path checkFileExists(final String absolutePath) + { + Objects.requireNonNull(absolutePath); + if(absolutePath.isBlank()) + { + throw new IllegalArgumentException("Empty path"); + } + + final Path path = Paths.get(absolutePath); + if(!Files.exists(path)) + { + throw new IllegalArgumentException("File does not exist at " + path); + } + return path; + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocation.java new file mode 100644 index 0000000..b159bc2 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocation.java @@ -0,0 +1,19 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file.absolute; + +import java.net.URL; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationType; +import software.xdev.pmd.model.config.thirdpartycplocation.file.FileThirdPartyCPLocation; + + +public class AbsoluteFileThirdPartyCPLocation extends FileThirdPartyCPLocation +{ + protected AbsoluteFileThirdPartyCPLocation( + final String id, + final URL url, + final String location, + final String absolutePath) + { + super(ThirdPartyCPLocationType.ABSOLUTE_FILE, id, url, location, absolutePath); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocationFactory.java new file mode 100644 index 0000000..63ceacf --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/absolute/AbsoluteFileThirdPartyCPLocationFactory.java @@ -0,0 +1,44 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file.absolute; + +import java.nio.file.Path; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.config.state.project.thirdpartycp.FileThirdPartyCPLocationState; +import software.xdev.pmd.model.config.thirdpartycplocation.file.FileThirdPartyCPLocationFactory; + + +public class AbsoluteFileThirdPartyCPLocationFactory + extends FileThirdPartyCPLocationFactory +{ + public AbsoluteFileThirdPartyCPLocationFactory(final Project project) + { + super(project); + } + + @Override + public AbsoluteFileThirdPartyCPLocation fromPersisted(final FileThirdPartyCPLocationState state) + { + final String absolutePath = this.projectFilePaths.toSystemPath(state.location()); + final Path path = this.checkFileExists(absolutePath); + + return new AbsoluteFileThirdPartyCPLocation( + state.id(), + this.pathToUrl(path), + state.location(), + absolutePath + ); + } + + @Override + public AbsoluteFileThirdPartyCPLocation fromUI(final Path uiState) + { + final String pathStr = uiState.toString(); + return new AbsoluteFileThirdPartyCPLocation( + this.newRandomId(), + this.pathToUrl(uiState), + this.projectFilePaths.toUnixPath(pathStr), + pathStr + ); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocation.java new file mode 100644 index 0000000..dc3d149 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocation.java @@ -0,0 +1,19 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file.relative; + +import java.net.URL; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationType; +import software.xdev.pmd.model.config.thirdpartycplocation.file.FileThirdPartyCPLocation; + + +public class RelativeFileThirdPartyCPLocation extends FileThirdPartyCPLocation +{ + protected RelativeFileThirdPartyCPLocation( + final String id, + final URL url, + final String location, + final String absolutePath) + { + super(ThirdPartyCPLocationType.RELATIVE_FILE, id, url, location, absolutePath); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocationFactory.java new file mode 100644 index 0000000..03bcae1 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/file/relative/RelativeFileThirdPartyCPLocationFactory.java @@ -0,0 +1,63 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.file.relative; + +import java.nio.file.Path; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.VirtualFile; + +import software.xdev.pmd.config.state.project.thirdpartycp.FileThirdPartyCPLocationState; +import software.xdev.pmd.model.config.thirdpartycplocation.file.FileThirdPartyCPLocationFactory; + + +public class RelativeFileThirdPartyCPLocationFactory + extends FileThirdPartyCPLocationFactory +{ + private final Project project; + + public RelativeFileThirdPartyCPLocationFactory(final Project project) + { + super(project); + this.project = project; + } + + @Override + public RelativeFileThirdPartyCPLocation fromPersisted(final FileThirdPartyCPLocationState state) + { + final String absolutePath = this.projectFilePaths.makeProjectRelativePathAbsolute( + this.projectFilePaths.toSystemPath(state.location())); + final Path path = this.checkFileExists(absolutePath); + + return new RelativeFileThirdPartyCPLocation( + state.id(), + this.pathToUrl(path), + state.location(), + absolutePath + ); + } + + @Override + public RelativeFileThirdPartyCPLocation fromUI(final Path uiState) + { + final Path projectPath = Objects.requireNonNull( + this.determineProjectPath(), + "Failed to determine project path"); + final Path relativePath = projectPath.relativize(uiState); + + return new RelativeFileThirdPartyCPLocation( + this.newRandomId(), + this.pathToUrl(uiState), + this.projectFilePaths.toUnixPath(relativePath.toString()), + uiState.toString() + ); + } + + private @Nullable Path determineProjectPath() + { + final VirtualFile projectDir = ProjectUtil.guessProjectDir(this.project); + return projectDir != null ? projectDir.toNioPath() : null; + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java new file mode 100644 index 0000000..9202d13 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.maven; + +import java.nio.CharBuffer; +import java.util.Objects; + +import org.jetbrains.annotations.NotNull; + + +public record MavenId( + String groupId, + String artifactId, + String version +) +{ + public MavenId + { + validateOrThrow(groupId); + validateOrThrow(artifactId); + validateOrThrow(version); + } + + // Does a general sanity check according to + // https://maven.apache.org/guides/mini/guide-naming-conventions.html + static void validateOrThrow(final String input) + { + Objects.requireNonNull(input); + if(input.isEmpty()) + { + throw new IllegalArgumentException("input is empty"); + } + + if(!CharBuffer.wrap(input).chars() + .allMatch(c -> c >= 'A' && c <= 'Z' + || c >= 'a' && c <= 'z' + || Character.isDigit(c) + || c == '.' + || c == '-' + || c == '_' + || c == '+' + )) + { + throw new IllegalArgumentException("Invalid input: " + input); + } + } + + @Override + public @NotNull String toString() + { + return this.groupId() + ":" + this.artifactId() + ":" + this.version(); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java new file mode 100644 index 0000000..e83a2dd --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java @@ -0,0 +1,54 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.maven; + +import java.net.URL; +import java.util.Objects; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationType; + + +public class MavenThirdPartyCPLocation extends ThirdPartyCPLocation +{ + private final MavenId mavenId; + + public MavenThirdPartyCPLocation( + final String id, + final URL url, + final MavenId mavenId) + { + super(ThirdPartyCPLocationType.MAVEN_ARTIFACT, id, url); + this.mavenId = mavenId; + } + + public MavenId mavenId() + { + return this.mavenId; + } + + @Override + public String displayLocation() + { + return this.mavenId.toString(); + } + + @Override + public boolean equals(final Object o) + { + if(o == null || this.getClass() != o.getClass()) + { + return false; + } + if(!super.equals(o)) + { + return false; + } + final MavenThirdPartyCPLocation that = (MavenThirdPartyCPLocation)o; + return Objects.equals(this.mavenId, that.mavenId); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), this.mavenId); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java new file mode 100644 index 0000000..fbba0f4 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.maven; + +import java.nio.file.Path; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.config.state.project.thirdpartycp.MavenThirdPartyCPLocationState; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationFactory; + + +public class MavenThirdPartyCPLocationFactory + extends ThirdPartyCPLocationFactory +{ + private final Project project; + + public MavenThirdPartyCPLocationFactory(final Project project) + { + this.project = project; + } + + @Override + public MavenThirdPartyCPLocation fromPersisted(final MavenThirdPartyCPLocationState state) + { + final MavenId mavenId = new MavenId(state.groupId(), state.artifactId(), state.version()); + final Path resolvedPath = this.resolveMavenArtifactId(mavenId); + + return new MavenThirdPartyCPLocation( + state.id(), + this.pathToUrl(resolvedPath), + mavenId + ); + } + + @Override + public MavenThirdPartyCPLocation fromUI(final MavenId uiState) + { + final Path resolvedPath = this.resolveMavenArtifactId(uiState); + + return new MavenThirdPartyCPLocation( + this.newRandomId(), + this.pathToUrl(resolvedPath), + uiState + ); + } + + protected Path resolveMavenArtifactId(final MavenId mavenId) + { + // TODO + return null; + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index 7f4b145..945a353 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -27,12 +27,11 @@ import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; import software.xdev.pmd.config.plugin.PatternContainer; -import software.xdev.pmd.config.plugin.ThirdPartyClasspathConfigContainer; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; import software.xdev.pmd.model.scope.ScanScope; import software.xdev.pmd.ui.config.project.components.exclusion.FileMaskPanelManager; -import software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanelManager; -import software.xdev.pmd.ui.config.project.components.thirdpartyclasspath.ThirdPartyClasspathPanelManager; +import software.xdev.pmd.ui.config.project.components.rulesetlocation.RSLocationPanelManager; +import software.xdev.pmd.ui.config.project.components.thirdpartyclasspath.TPCPLocationPanelManager; /** @@ -50,7 +49,7 @@ public class PMDConfigPanel extends JPanel private final JBCheckBox chbxUseCacheFile = new JBCheckBox("Use cache file"); private final JBCheckBox chbxImportSettingsFromMaven = new JBCheckBox("Import settings from Maven"); - private final LocationPanelManager rulefileLocationPanelManager; + private final RSLocationPanelManager rsLocationPanelManager; private final FileMaskPanelManager exclusionPanelManager = new FileMaskPanelManager( "Exclusions", @@ -71,7 +70,7 @@

Ignores certain files (patterns). """ ); - private final ThirdPartyClasspathPanelManager thirdPartyClasspathPanelManager; + private final TPCPLocationPanelManager tpcpLocationPanelManager; final Project project; @@ -81,8 +80,8 @@ public PMDConfigPanel(@NotNull final Project project) this.project = project; - this.rulefileLocationPanelManager = new LocationPanelManager(project, this); - this.thirdPartyClasspathPanelManager = new ThirdPartyClasspathPanelManager(project, this); + this.rsLocationPanelManager = new RSLocationPanelManager(project, this); + this.tpcpLocationPanelManager = new TPCPLocationPanelManager(project, this); this.initialise(); } @@ -123,7 +122,7 @@ private JPanel buildConfigPanel() this.addPanel( configFilePanel, - this.rulefileLocationPanelManager.buildPanel(), + this.rsLocationPanelManager.panel(), 3, 1.0, 300); @@ -137,7 +136,7 @@ private JPanel buildConfigPanel() this.addPanel( configFilePanel, - this.thirdPartyClasspathPanelManager.buildPanel(), + this.tpcpLocationPanelManager.panel(), 5, 0.5, 150); @@ -210,13 +209,12 @@ public void showPluginConfiguration(@NotNull final PluginConfiguration pluginCon this.chbxShowSuppressedWarnings.setSelected(pluginConfig.showSuppressedWarnings()); this.chbxUseCacheFile.setSelected(pluginConfig.useCacheFile()); this.chbxImportSettingsFromMaven.setSelected(pluginConfig.importSettingsFromMaven()); - this.rulefileLocationPanelManager.locationModel().setLocations(new ArrayList<>(pluginConfig.locations())); - this.rulefileLocationPanelManager.locationModel().setActiveLocations(pluginConfig.getActiveLocations()); + this.rsLocationPanelManager.locationModel().setLocations(new ArrayList<>(pluginConfig.locations())); + this.rsLocationPanelManager.locationModel().setActiveLocations(pluginConfig.getActiveLocations()); this.exclusionPanelManager.update(pluginConfig.projectRelativeFileExclusions().stream() .map(PatternContainer::patternString) .collect(Collectors.toCollection(TreeSet::new))); - this.thirdPartyClasspathPanelManager.setThirdPartyClassPath( - pluginConfig.thirdPartyClasspath().absolutePathStrings()); + this.tpcpLocationPanelManager.locationModel().setLocations(pluginConfig.thirdPartyCPLocations()); } public PluginConfiguration getPluginConfiguration() @@ -229,14 +227,11 @@ public PluginConfiguration getPluginConfiguration() (ScanScope)this.cbScope.getSelectedItem(), ScanScope::getDefaultValue)) .withProjectRelativeFileExclusionsRaw(this.exclusionPanelManager.getPatterns()) - .withLocations(new TreeSet<>(this.rulefileLocationPanelManager.locationModel().getLocations())) - .withActiveLocationIds(this.rulefileLocationPanelManager.locationModel().getActiveLocations().stream() + .withLocations(new TreeSet<>(this.rsLocationPanelManager.locationModel().getLocations())) + .withActiveLocationIds(this.rsLocationPanelManager.locationModel().getActiveLocations().stream() .map(ConfigurationLocation::getId) .collect(Collectors.toCollection(TreeSet::new))) - .withThirdPartyClasspath( - ThirdPartyClasspathConfigContainer.createFromUI( - this.project, - this.thirdPartyClasspathPanelManager.getThirdPartyClassPath())) + .withThirdPartyCPLocations(this.tpcpLocationPanelManager.locationModel().getLocations()) .withImportSettingFromMaven(this.chbxImportSettingsFromMaven.isSelected()) .build(); } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java deleted file mode 100644 index 80a3eb8..0000000 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanelManager.java +++ /dev/null @@ -1,159 +0,0 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; - -import java.awt.BorderLayout; -import java.awt.Dialog; - -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTable; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.table.TableColumn; - -import org.jetbrains.annotations.NotNull; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.ui.AnActionButton; -import com.intellij.ui.AnActionButtonRunnable; -import com.intellij.ui.AnActionButtonUpdater; -import com.intellij.ui.TitledSeparator; -import com.intellij.ui.ToolbarDecorator; -import com.intellij.ui.table.JBTable; -import com.intellij.util.ui.JBUI; - -import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; -import software.xdev.pmd.ui.config.project.PMDConfigPanel; -import software.xdev.pmd.ui.config.project.components.SubPMDConfigPanelManager; - - -public class LocationPanelManager extends SubPMDConfigPanelManager -{ - private static final int ACTIVE_COL_MIN_WIDTH = 40; - private static final int ACTIVE_COL_MAX_WIDTH = 55; - private static final int DESC_COL_MIN_WIDTH = 100; - private static final int DESC_COL_MAX_WIDTH = 200; - - final LocationTableModel locationModel = new LocationTableModel(); - final JBTable locationTable = new JBTable(this.locationModel); - - public LocationPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) - { - super(project, pmdConfigPanel); - } - - public JPanel buildPanel() - { - this.setColumnWith(this.locationTable, 0, ACTIVE_COL_MIN_WIDTH, ACTIVE_COL_MAX_WIDTH, ACTIVE_COL_MAX_WIDTH); - this.setColumnWith(this.locationTable, 1, DESC_COL_MIN_WIDTH, DESC_COL_MAX_WIDTH, DESC_COL_MAX_WIDTH); - this.locationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); - this.locationTable.setStriped(true); - this.locationTable.getTableHeader().setReorderingAllowed(false); - - final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable); - tableDecorator.setAddAction(new AddLocationAction()); - tableDecorator.setRemoveAction(new RemoveLocationAction()); - tableDecorator.setEditActionUpdater(new EnableWhenSelected()); - tableDecorator.setRemoveActionUpdater(new EnableWhenSelectedAndRemovable()); - tableDecorator.setPreferredSize(DECORATOR_DIMENSIONS); - - final JPanel container = new JPanel(new BorderLayout()); - container.add(new TitledSeparator("Configuration File"), BorderLayout.NORTH); - container.add(tableDecorator.createPanel(), BorderLayout.CENTER); - final JLabel infoLabel = new JLabel( - "The active rules may be overridden or deactivated by module settings", - AllIcons.General.Information, SwingConstants.LEFT); - infoLabel.setBorder(JBUI.Borders.empty(8, 0, 4, 0)); - container.add(infoLabel, BorderLayout.SOUTH); - return container; - } - - private void setColumnWith( - final JTable table, - final int columnIndex, - final int minSize, - final int preferredSize, - final Integer maxSize) - { - final TableColumn column = table.getColumnModel().getColumn(columnIndex); - column.setMinWidth(minSize); - column.setWidth(preferredSize); - column.setPreferredWidth(preferredSize); - if(maxSize != null) - { - column.setMaxWidth(maxSize); - } - } - - public LocationTableModel locationModel() - { - return this.locationModel; - } - - class AddLocationAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final LocationDialog dialogue = new LocationDialog( - (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, LocationPanelManager.this.pmdConfigPanel), - LocationPanelManager.this.project); - - if(dialogue.showAndGet()) - { - final ConfigurationLocation newLocation = dialogue.getConfigurationLocation(); - if(LocationPanelManager.this.locationModel.getLocations().contains(newLocation)) - { - Messages.showWarningDialog( - LocationPanelManager.this.project, - "This location has already been added", - "Duplicate Location"); - } - else - { - LocationPanelManager.this.locationModel.addLocation(dialogue.getConfigurationLocation()); - } - } - } - } - - - class RemoveLocationAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final int selectedIndex = LocationPanelManager.this.locationTable.getSelectedRow(); - if(selectedIndex == -1) - { - return; - } - - LocationPanelManager.this.locationModel.removeLocationAt(selectedIndex); - } - } - - - class EnableWhenSelectedAndRemovable implements AnActionButtonUpdater - { - @Override - public boolean isEnabled(@NotNull final AnActionEvent e) - { - final int selectedItem = LocationPanelManager.this.locationTable.getSelectedRow(); - return selectedItem >= 0 - && LocationPanelManager.this.locationModel.getLocationAt(selectedItem).isRemovable(); - } - } - - - class EnableWhenSelected implements AnActionButtonUpdater - { - @Override - public boolean isEnabled(@NotNull final AnActionEvent e) - { - return LocationPanelManager.this.locationTable.getSelectedRow() >= 0; - } - } -} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSErrorPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSErrorPanel.java new file mode 100644 index 0000000..709c25e --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSErrorPanel.java @@ -0,0 +1,19 @@ +package software.xdev.pmd.ui.config.project.components.rulesetlocation; + +import software.xdev.pmd.ui.config.project.components.shared.ErrorPanel; + + +public class RSErrorPanel extends ErrorPanel +{ + @Override + protected Throwable extractCause(final Throwable t) + { + if(t.getCause() != null + && t.getCause() != t + && !t.getClass().getPackage().getName().startsWith("net.sourceforge.pmd")) + { + return this.extractCause(t.getCause()); + } + return t; + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationDialog.java new file mode 100644 index 0000000..9af6d01 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationDialog.java @@ -0,0 +1,57 @@ +package software.xdev.pmd.ui.config.project.components.rulesetlocation; + +import java.awt.Dialog; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.analysis.ProjectScanClasspathManager; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.ui.config.project.components.shared.LocationDialog; + + +@SuppressWarnings("checkstyle:MagicNumber") +public class RSLocationDialog + extends LocationDialog +{ + public RSLocationDialog( + @Nullable final Dialog parent, + @NotNull final Project project) + { + super(parent, project, new RSLocationPanel(project)); + this.setErrorPanel(new RSErrorPanel()); + } + + @Override + protected ConfigurationLocation getLocationFromPanelAndValidate(final RSLocationPanel panel) throws Exception + { + final ConfigurationLocation location; + try + { + location = this.locationPanel.getConfigurationLocation(); + } + catch(final Exception ex) + { + this.showError("Failed to get configuration: " + ex.getMessage()); + this.logger.debug("Failed to get configuration", ex); + return null; + } + if(location == null) + { + this.showError("No location has been entered"); + return null; + } + + if(location.getDescription() == null || location.getDescription().isEmpty()) + { + this.showError("No description has been entered"); + return null; + } + + location.validate(this.project.getService(ProjectScanClasspathManager.class).getClassLoader()); + + return location; + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanel.java similarity index 87% rename from src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanel.java index d8711cb..80cac5c 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanel.java @@ -1,8 +1,8 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; +package software.xdev.pmd.ui.config.project.components.rulesetlocation; import static software.xdev.pmd.model.config.rulesetlocation.ConfigurationType.LOCAL_FILE; import static software.xdev.pmd.model.config.rulesetlocation.ConfigurationType.PROJECT_RELATIVE; -import static software.xdev.pmd.ui.config.project.components.rulefilelocation.LocationPanel.LocationType.FILE; +import static software.xdev.pmd.ui.config.project.components.rulesetlocation.RSLocationPanel.LocationType.FILE; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -44,7 +44,7 @@ @SuppressWarnings("checkstyle:MagicNumber") -public class LocationPanel extends JPanel +public class RSLocationPanel extends JPanel { enum LocationType { @@ -62,7 +62,7 @@ enum LocationType private final Project project; - public LocationPanel(final Project project) + public RSLocationPanel(final Project project) { super(new GridBagLayout()); @@ -75,9 +75,10 @@ private void initialise() { this.relativeFileCheckbox.setText("Store relative to project location"); this.relativeFileCheckbox.setToolTipText("The file path should be stored as relative to the project location"); + this.relativeFileCheckbox.setSelected(true); this.fileLocationRadio.setText("Use a local file"); - this.fileLocationRadio.addActionListener(new RadioButtonActionListener()); + this.fileLocationRadio.addActionListener(this.createRadioButtonListener(FILE)); final ButtonGroup locationGroup = new ButtonGroup(); locationGroup.add(this.fileLocationRadio); @@ -130,11 +131,18 @@ this.relativeFileCheckbox, new GridBagConstraints( GridBagConstraints.WEST, GridBagConstraints.VERTICAL, COMPONENT_INSETS, 0, 0)); } + private ActionListener createRadioButtonListener(final LocationType locationType) + { + return e -> this.enabledLocation(locationType); + } + private void enabledLocation(final LocationType locationType) { - this.fileLocationField.setEnabled(locationType == FILE); - this.browseButton.setEnabled(locationType == FILE); - this.relativeFileCheckbox.setEnabled(locationType == FILE); + final boolean isFile = locationType == FILE; + + this.fileLocationField.setEnabled(isFile); + this.browseButton.setEnabled(isFile); + this.relativeFileCheckbox.setEnabled(isFile); } private ConfigurationType typeOfFile() @@ -251,7 +259,7 @@ public void actionPerformed(final ActionEvent e) Optional fileLocationPath; try { - fileLocationPath = Optional.ofNullable(LocationPanel.this.getFileLocationPath(LOCAL_FILE)); + fileLocationPath = Optional.ofNullable(RSLocationPanel.this.getFileLocationPath(LOCAL_FILE)); } catch(final Exception ex) { @@ -259,32 +267,18 @@ public void actionPerformed(final ActionEvent e) } final VirtualFile toSelect = fileLocationPath .map(LocalFileSystem.getInstance()::findFileByNioFile) - .orElseGet(() -> ProjectUtil.guessProjectDir(LocationPanel.this.project)); + .orElseGet(() -> ProjectUtil.guessProjectDir(RSLocationPanel.this.project)); final VirtualFile chosen = FileChooser.chooseFile( FileChooserDescriptorFactory.createSingleFileDescriptor("xml"), - LocationPanel.this, - LocationPanel.this.project, + RSLocationPanel.this, + RSLocationPanel.this.project, toSelect); if(chosen != null) { final File newConfigFile = VfsUtilCore.virtualToIoFile(chosen); - LocationPanel.this.fileLocationField.setText(newConfigFile.getAbsolutePath()); - } - } - } - - - private final class RadioButtonActionListener implements ActionListener - { - @Override - public void actionPerformed(final ActionEvent e) - { - if(LocationPanel.this.fileLocationRadio.isSelected()) - { - LocationPanel.this.enabledLocation(FILE); + RSLocationPanel.this.fileLocationField.setText(newConfigFile.getAbsolutePath()); } - throw new IllegalStateException("Unknown radio button state"); } } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanelManager.java new file mode 100644 index 0000000..7d6c1c5 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanelManager.java @@ -0,0 +1,81 @@ +package software.xdev.pmd.ui.config.project.components.rulesetlocation; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import org.jetbrains.annotations.NotNull; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.ui.AnActionButtonUpdater; +import com.intellij.ui.TitledSeparator; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.util.ui.JBUI; + +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.ui.config.project.PMDConfigPanel; +import software.xdev.pmd.ui.config.project.components.shared.LocationPanelManager; + + +public class RSLocationPanelManager + extends LocationPanelManager +{ + private static final int ACTIVE_COL_MIN_WIDTH = 40; + private static final int ACTIVE_COL_MAX_WIDTH = 55; + private static final int DESC_COL_MIN_WIDTH = 100; + private static final int DESC_COL_MAX_WIDTH = 200; + + public RSLocationPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) + { + super(project, pmdConfigPanel, new RSLocationTableModel(), RSLocationDialog::new); + } + + @Override + public JPanel panel() + { + this.setColumnWith(this.locationTable, 0, ACTIVE_COL_MIN_WIDTH, ACTIVE_COL_MAX_WIDTH, ACTIVE_COL_MAX_WIDTH); + this.setColumnWith(this.locationTable, 1, DESC_COL_MIN_WIDTH, DESC_COL_MAX_WIDTH, DESC_COL_MAX_WIDTH); + this.configureLocationTableDefaults(); + + final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable) + .setAddAction(new AddLocationAction()) + .setRemoveAction(new RemoveLocationAction()) + .setEditActionUpdater(new EnableWhenSelected()) + .setRemoveActionUpdater(new EnableWhenSelectedAndRemovable()) + .setPreferredSize(DECORATOR_DIMENSIONS); + + final JPanel container = new JPanel(new BorderLayout()); + container.add(new TitledSeparator("Configuration File"), BorderLayout.NORTH); + container.add(tableDecorator.createPanel(), BorderLayout.CENTER); + final JLabel infoLabel = new JLabel( + "The active rules may be overridden or deactivated by module settings", + AllIcons.General.Information, SwingConstants.LEFT); + infoLabel.setBorder(JBUI.Borders.empty(8, 0, 4, 0)); + container.add(infoLabel, BorderLayout.SOUTH); + return container; + } + + class EnableWhenSelectedAndRemovable implements AnActionButtonUpdater + { + @Override + public boolean isEnabled(@NotNull final AnActionEvent e) + { + final int selectedItem = RSLocationPanelManager.this.locationTable.getSelectedRow(); + return selectedItem >= 0 + && RSLocationPanelManager.this.locationModel.getLocationAt(selectedItem).isRemovable(); + } + } + + class EnableWhenSelected implements AnActionButtonUpdater + { + @Override + public boolean isEnabled(@NotNull final AnActionEvent e) + { + return RSLocationPanelManager.this.locationTable.getSelectedRow() >= 0; + } + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationTableModel.java similarity index 82% rename from src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationTableModel.java index e6b42f2..9ef456d 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationTableModel.java @@ -1,34 +1,28 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; +package software.xdev.pmd.ui.config.project.components.rulesetlocation; import static java.util.function.Predicate.not; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; -import javax.swing.table.AbstractTableModel; - import org.jetbrains.annotations.NotNull; import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.ui.config.project.components.shared.LocationTableModel; -/** - * A table model for editing CheckStyle file locations. - */ -public class LocationTableModel extends AbstractTableModel +public class RSLocationTableModel extends LocationTableModel { private static final int COLUMN_ACTIVE = 0; private static final int COLUMN_DESCRIPTION = 1; private static final int COLUMN_LOCATION = 2; private static final int NUMBER_OF_COLUMNS = 3; - private final List locations = new ArrayList<>(); private final SortedSet activeLocations = new TreeSet<>(); + @Override public void setLocations(final List newLocations) { this.locations.clear(); @@ -43,15 +37,7 @@ public void setLocations(final List newLocations) this.fireTableDataChanged(); } - public void addLocation(final ConfigurationLocation location) - { - if(location != null) - { - this.locations.add(location); - this.fireTableRowsInserted(this.locations.size() - 1, this.locations.size() - 1); - } - } - + @Override public void removeLocationAt(final int index) { final ConfigurationLocation locationToRemove = this.locations.get(index); @@ -66,11 +52,6 @@ public void removeLocationAt(final int index) this.fireTableRowsDeleted(index, index); } - public ConfigurationLocation getLocationAt(final int index) - { - return this.locations.get(index); - } - public void setActiveLocations(@NotNull final SortedSet activeLocations) { if(!activeLocations.isEmpty() && !new HashSet<>(this.locations).containsAll(activeLocations)) @@ -115,11 +96,6 @@ public SortedSet getActiveLocations() return this.activeLocations; } - public List getLocations() - { - return Collections.unmodifiableList(this.locations); - } - @Override public int getColumnCount() { diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/CompletePanel.java similarity index 77% rename from src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/CompletePanel.java index 0a46d57..e63e64f 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/CompletePanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/CompletePanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; +package software.xdev.pmd.ui.config.project.components.shared; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -21,7 +21,7 @@ public CompletePanel() private void init() { - final JLabel infoLabel = new JLabel("The file has been validated and is ready to add"); + final JLabel infoLabel = new JLabel("Validation successful and ready to add"); this.setBorder(JBUI.Borders.empty(4)); diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/ErrorPanel.java similarity index 76% rename from src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/ErrorPanel.java index 4fbc473..3cc6af9 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/ErrorPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/ErrorPanel.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; +package software.xdev.pmd.ui.config.project.components.shared; import static javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS; import static javax.swing.JScrollPane.VERTICAL_SCROLLBAR_ALWAYS; @@ -34,7 +34,7 @@ private void init() { this.setBorder(JBUI.Borders.empty(8)); - final JLabel infoLabel = new JLabel("Loading the rule file caused an error:"); + final JLabel infoLabel = new JLabel("Validation failed:"); infoLabel.setBorder(JBUI.Borders.emptyBottom(8)); this.add(infoLabel, BorderLayout.NORTH); @@ -51,21 +51,15 @@ private void init() public void setError(final Throwable t) { final StringWriter errorWriter = new StringWriter(256); - this.causeOf(t).printStackTrace(new PrintWriter(errorWriter)); + this.extractCause(t).printStackTrace(new PrintWriter(errorWriter)); this.errorField.setText(errorWriter.getBuffer().toString()); this.errorField.setCaretPosition(0); this.invalidate(); } - private Throwable causeOf(final Throwable t) + protected Throwable extractCause(final Throwable t) { - if(t.getCause() != null - && t.getCause() != t - && !t.getClass().getPackage().getName().startsWith("net.sourceforge.pmd")) - { - return this.causeOf(t.getCause()); - } return t; } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java similarity index 68% rename from src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java index 9d9531a..c73689d 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/rulefilelocation/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.ui.config.project.components.rulefilelocation; +package software.xdev.pmd.ui.config.project.components.shared; import java.awt.BorderLayout; import java.awt.Dialog; @@ -23,24 +23,16 @@ import com.intellij.openapi.ui.Messages; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.analysis.ProjectScanClasspathManager; -import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; - -/** - * Allows selection of the location of the file. - */ @SuppressWarnings("checkstyle:MagicNumber") -public class LocationDialog extends DialogWrapper +public abstract class LocationDialog extends DialogWrapper { - private static final Logger LOG = Logger.getInstance(LocationDialog.class); - private static final Insets COMPONENT_INSETS = JBUI.insets(4); private static final int WIDTH = 500; private static final int HEIGHT = 400; - private enum Step + protected enum Step { SELECT(false, true, false), ERROR(true, false, false), @@ -74,40 +66,49 @@ private boolean isAllowCommit() } - private final Project project; + protected Logger logger; + + protected final Project project; - private final JPanel centrePanel = new JPanel(new BorderLayout()); - private final LocationPanel locationPanel; - private final ErrorPanel errorPanel = new ErrorPanel(); - private final CompletePanel completePanel = new CompletePanel(); + protected final JPanel centrePanel = new JPanel(new BorderLayout()); + protected final P locationPanel; + protected ErrorPanel errorPanel = new ErrorPanel(); + protected final CompletePanel completePanel = new CompletePanel(); - private final JButton commitButton = new JButton(new NextAction()); - private final JButton previousButton = new JButton(new PreviousAction()); + protected final JButton commitButton = new JButton(new NextAction()); + protected final JButton previousButton = new JButton(new PreviousAction()); - private Step currentStep = Step.SELECT; + protected Step currentStep = Step.SELECT; - private ConfigurationLocation configurationLocation; + protected L configurationLocation; - public LocationDialog( + protected LocationDialog( @Nullable final Dialog parent, - @NotNull final Project project) + @NotNull final Project project, + @NotNull final P locationPanel) { super(project, parent, false, IdeModalityType.IDE); - this.project = project; + this.logger = Logger.getInstance(this.getClass()); - this.locationPanel = new LocationPanel(project); + this.project = project; + this.locationPanel = locationPanel; this.initialiseComponents(); } + protected void setErrorPanel(final ErrorPanel errorPanel) + { + this.errorPanel = errorPanel; + } + @Override protected @Nullable JComponent createCenterPanel() { return this.centrePanel; } - private void initialiseComponents() + protected void initialiseComponents() { this.setTitle("Add Configuration"); this.setSize(WIDTH, HEIGHT); @@ -147,7 +148,7 @@ this.commitButton, new GridBagConstraints( return bottomPanel; } - private JPanel panelForCurrentStep() + protected JPanel panelForCurrentStep() { return switch(this.currentStep) { @@ -162,12 +163,12 @@ private JPanel panelForCurrentStep() * * @return the location or null if no valid location entered. */ - public ConfigurationLocation getConfigurationLocation() + public L getConfigurationLocation() { return this.configurationLocation; } - private void moveToStep(final Step newStep) + protected void moveToStep(final Step newStep) { this.centrePanel.remove(this.panelForCurrentStep()); this.currentStep = newStep; @@ -184,24 +185,7 @@ private void moveToStep(final Step newStep) this.centrePanel.repaint(); } - private Step continueWithValidate(final ConfigurationLocation location) - { - this.configurationLocation = location; - - try - { - this.configurationLocation.validate( - this.project.getService(ProjectScanClasspathManager.class).getClassLoader()); - return Step.COMPLETE; - } - catch(final Exception e) - { - this.errorPanel.setError(e); - return Step.ERROR; - } - } - - void onPrevious() + protected void onPrevious() { this.previousButton.setEnabled(false); @@ -217,37 +201,32 @@ void onPrevious() } } - void onNext() + protected abstract L getLocationFromPanelAndValidate(P panel) throws Exception; + + protected void onNext() { this.commitButton.setEnabled(false); - final ConfigurationLocation location; switch(this.currentStep) { case SELECT: try { - location = this.locationPanel.getConfigurationLocation(); + final L location = this.getLocationFromPanelAndValidate(this.locationPanel); + if(location == null) + { + return; + } + + this.configurationLocation = location; + this.moveToStep(Step.COMPLETE); } - catch(final Exception ex) + catch(final Exception e) { - this.showError("Failed to get configuration: " + ex.getMessage()); - LOG.debug("Failed to get configuration", ex); - return; - } - if(location == null) - { - this.showError("No location has been entered"); - return; + this.errorPanel.setError(e); + this.moveToStep(Step.ERROR); } - if(location.getDescription() == null || location.getDescription().isEmpty()) - { - this.showError("No description has been entered"); - return; - } - - this.moveToStep(this.continueWithValidate(location)); return; case COMPLETE: @@ -260,7 +239,7 @@ void onNext() } } - private void showError(final String formattedMessage) + protected void showError(final String formattedMessage) { Messages.showErrorDialog( this.getContentPanel(), @@ -269,7 +248,7 @@ private void showError(final String formattedMessage) this.commitButton.setEnabled(true); } - private final class NextAction extends AbstractAction + protected final class NextAction extends AbstractAction { @Override public void actionPerformed(final ActionEvent event) @@ -278,9 +257,10 @@ public void actionPerformed(final ActionEvent event) } } - private class PreviousAction extends AbstractAction + + protected class PreviousAction extends AbstractAction { - PreviousAction() + protected PreviousAction() { this.putValue(Action.NAME, "Previous"); this.putValue(Action.SHORT_DESCRIPTION, "Move to the previous step of the wizard"); diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationPanelManager.java new file mode 100644 index 0000000..bf6dac7 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationPanelManager.java @@ -0,0 +1,120 @@ +package software.xdev.pmd.ui.config.project.components.shared; + +import java.awt.Dialog; +import java.awt.Dimension; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.table.TableColumn; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.AnActionButtonRunnable; +import com.intellij.ui.table.JBTable; + +import software.xdev.pmd.ui.config.project.PMDConfigPanel; + + +public abstract class LocationPanelManager< + M extends LocationTableModel, + D extends LocationDialog, + T> +{ + protected static final Dimension DECORATOR_DIMENSIONS = new Dimension(300, 50); + + protected Project project; + protected final M locationModel; + protected final JBTable locationTable; + protected final Supplier dialogCreator; + + protected LocationPanelManager( + final Project project, + final PMDConfigPanel pmdConfigPanel, + final M locationModel, + final BiFunction createDialogFunc) + { + this.project = project; + + this.locationModel = locationModel; + this.locationTable = new JBTable(this.locationModel); + + this.dialogCreator = () -> createDialogFunc.apply( + (Dialog)SwingUtilities.getAncestorOfClass(Dialog.class, pmdConfigPanel), + project); + } + + protected void configureLocationTableDefaults() + { + this.locationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + this.locationTable.setStriped(true); + this.locationTable.getTableHeader().setReorderingAllowed(false); + } + + public abstract JPanel panel(); + + protected void setColumnWith( + final JTable table, + final int columnIndex, + final int minSize, + final int preferredSize, + final Integer maxSize) + { + final TableColumn column = table.getColumnModel().getColumn(columnIndex); + column.setMinWidth(minSize); + column.setWidth(preferredSize); + column.setPreferredWidth(preferredSize); + if(maxSize != null) + { + column.setMaxWidth(maxSize); + } + } + + public M locationModel() + { + return this.locationModel; + } + + public class AddLocationAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final D dialog = LocationPanelManager.this.dialogCreator.get(); + if(dialog.showAndGet()) + { + final T newLocation = dialog.getConfigurationLocation(); + if(LocationPanelManager.this.locationModel.getLocations().contains(newLocation)) + { + Messages.showWarningDialog( + LocationPanelManager.this.project, + "This location has already been added", + "Duplicate Location"); + } + else + { + LocationPanelManager.this.locationModel.addLocation(dialog.getConfigurationLocation()); + } + } + } + } + + + public class RemoveLocationAction implements AnActionButtonRunnable + { + @Override + public void run(final AnActionButton anActionButton) + { + final int selectedIndex = LocationPanelManager.this.locationTable.getSelectedRow(); + if(selectedIndex == -1) + { + return; + } + + LocationPanelManager.this.locationModel.removeLocationAt(selectedIndex); + } + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationTableModel.java new file mode 100644 index 0000000..cdb52d9 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationTableModel.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.ui.config.project.components.shared; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + + +public abstract class LocationTableModel extends AbstractTableModel +{ + protected final List locations = new ArrayList<>(); + + public void setLocations(final List newLocations) + { + this.locations.clear(); + + if(newLocations != null) + { + this.locations.addAll(newLocations); + } + + this.fireTableDataChanged(); + } + + public void addLocation(final T location) + { + if(location != null) + { + this.locations.add(location); + this.fireTableRowsInserted(this.locations.size() - 1, this.locations.size() - 1); + } + } + + public void removeLocationAt(final int index) + { + this.locations.remove(index); + + this.fireTableRowsDeleted(index, index); + } + + public T getLocationAt(final int index) + { + return this.locations.get(index); + } + + public List getLocations() + { + return Collections.unmodifiableList(this.locations); + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationDialog.java new file mode 100644 index 0000000..03cef7b --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationDialog.java @@ -0,0 +1,28 @@ +package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; + +import java.awt.Dialog; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.ui.config.project.components.shared.LocationDialog; + + +public class TPCPLocationDialog extends LocationDialog +{ + public TPCPLocationDialog( + @Nullable final Dialog parent, + @NotNull final Project project) + { + super(parent, project, new TPCPLocationPanel(project)); + } + + @Override + protected ThirdPartyCPLocation getLocationFromPanelAndValidate(final TPCPLocationPanel panel) + { + return panel.createLocation(); + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java new file mode 100644 index 0000000..11ad912 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java @@ -0,0 +1,265 @@ +package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Box; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTextField; + +import com.intellij.openapi.fileChooser.FileChooser; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.JBUI; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.file.absolute.AbsoluteFileThirdPartyCPLocationFactory; +import software.xdev.pmd.model.config.thirdpartycplocation.file.relative.RelativeFileThirdPartyCPLocationFactory; +import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenId; +import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenThirdPartyCPLocationFactory; + + +@SuppressWarnings("checkstyle:MagicNumber") +public class TPCPLocationPanel extends JPanel +{ + enum LocationType + { + FILE, MAVEN_ARTIFACT + } + + + private static final Insets COMPONENT_INSETS = JBUI.insets(4); + + private final JRadioButton radioFileLocation = new JRadioButton(); + private final JTextField txtFileLocation = new JTextField(20); + private final JButton btnBrowse = new JButton(new BrowseAction()); + private final JCheckBox chbxRelativeFile = new JCheckBox(); + + private final JRadioButton radioMavenArtifact = new JRadioButton(); + private final JTextField txtMavenArtifactGroupId = new JTextField(20); + private final JTextField txtMavenArtifactArtifactId = new JTextField(20); + private final JTextField txtMavenArtifactVersion = new JTextField(20); + + private final Project project; + + public TPCPLocationPanel(final Project project) + { + super(new GridBagLayout()); + + this.project = Objects.requireNonNull(project); + + this.initialise(); + } + + private void initialise() + { + this.chbxRelativeFile.setText("Store relative to project location"); + this.chbxRelativeFile.setToolTipText("The file path should be stored as relative to the project location"); + this.chbxRelativeFile.setSelected(true); + + this.radioFileLocation.setText("Use a local file"); + this.radioFileLocation.addActionListener(this.createRadioButtonListener(LocationType.FILE)); + this.radioMavenArtifact.setText("Use a Maven artifact"); + this.radioMavenArtifact.addActionListener(this.createRadioButtonListener(LocationType.MAVEN_ARTIFACT)); + + final ButtonGroup locationGroup = new ButtonGroup(); + locationGroup.add(this.radioFileLocation); + locationGroup.add(this.radioMavenArtifact); + + this.radioFileLocation.setSelected(true); + this.enabledLocation(LocationType.FILE); + + this.setBorder(JBUI.Borders.empty(8, 8, 4, 8)); + + int gridY = 0; + this.add( + this.radioFileLocation, new GridBagConstraints( + 0, gridY, 3, 1, 0.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + + gridY++; + this.add( + new JLabel("File:"), new GridBagConstraints( + 0, gridY, 1, 1, 0.0, 0.0, + GridBagConstraints.EAST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + this.add( + this.txtFileLocation, new GridBagConstraints( + 1, gridY, 1, 1, 1.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, COMPONENT_INSETS, 0, 0)); + this.add( + this.btnBrowse, new GridBagConstraints( + 2, gridY, 1, 1, 0.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + + gridY++; + this.add( + this.chbxRelativeFile, new GridBagConstraints( + 1, gridY, 2, 1, 0.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + + gridY++; + this.add( + this.radioMavenArtifact, new GridBagConstraints( + 0, gridY, 3, 1, 0.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + + this.addTextFieldLine(++gridY, "GroupId", this.txtMavenArtifactGroupId); + this.addTextFieldLine(++gridY, "ArtifactId", this.txtMavenArtifactArtifactId); + this.addTextFieldLine(++gridY, "Version", this.txtMavenArtifactVersion); + + this.add( + Box.createVerticalGlue(), new GridBagConstraints( + 0, ++gridY, 3, 1, 0.0, 1.0, + GridBagConstraints.WEST, GridBagConstraints.VERTICAL, COMPONENT_INSETS, 0, 0)); + } + + private void addTextFieldLine(final int gridY, final String label, final JTextField textField) + { + this.add( + new JLabel(label + ":"), new GridBagConstraints( + 0, gridY, 1, 1, 0.0, 0.0, + GridBagConstraints.EAST, GridBagConstraints.NONE, COMPONENT_INSETS, 0, 0)); + this.add( + textField, new GridBagConstraints( + 1, gridY, 2, 1, 1.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, COMPONENT_INSETS, 0, 0)); + } + + private ActionListener createRadioButtonListener(final LocationType locationType) + { + return e -> this.enabledLocation(locationType); + } + + private void enabledLocation(final LocationType locationType) + { + final boolean isFile = locationType == LocationType.FILE; + this.txtFileLocation.setEnabled(isFile); + this.btnBrowse.setEnabled(isFile); + this.chbxRelativeFile.setEnabled(isFile); + + final boolean isMavenArtifact = locationType == LocationType.MAVEN_ARTIFACT; + this.txtMavenArtifactGroupId.setEnabled(isMavenArtifact); + this.txtMavenArtifactArtifactId.setEnabled(isMavenArtifact); + this.txtMavenArtifactVersion.setEnabled(isMavenArtifact); + } + + public ThirdPartyCPLocation createLocation() + { + if(this.txtFileLocation.isEnabled() && this.isNotBlank(this.txtFileLocation.getText())) + { + return (this.chbxRelativeFile.isSelected() + ? this.project.getService(RelativeFileThirdPartyCPLocationFactory.class) + : this.project.getService(AbsoluteFileThirdPartyCPLocationFactory.class)) + .fromUI(this.getAbsoluteFileLocationPath()); + } + else if(this.txtMavenArtifactGroupId.isEnabled() + && this.isNotBlank(this.txtMavenArtifactGroupId.getText()) + && this.isNotBlank(this.txtMavenArtifactArtifactId.getText()) + && this.isNotBlank(this.txtMavenArtifactVersion.getText())) + { + this.project.getService(MavenThirdPartyCPLocationFactory.class) + .fromUI(new MavenId( + this.txtMavenArtifactGroupId.getText(), + this.txtMavenArtifactArtifactId.getText(), + this.txtMavenArtifactVersion.getText())); + } + + return null; + } + + private Path getAbsoluteFileLocationPath() + { + final String pathStr = this.txtFileLocation.getText(); + Objects.requireNonNull(pathStr); + if(pathStr.isBlank()) + { + throw new IllegalArgumentException("Invalid path: " + pathStr); + } + + final Path path = Paths.get(pathStr.trim()); + if(!path.isAbsolute()) + { + throw new IllegalArgumentException("Non absolute path: " + path); + } + + if(!Files.exists(path)) + { + throw new IllegalArgumentException("File does not exist: " + path); + } + + return path; + } + + private boolean isNotBlank(final String str) + { + return str != null && !str.isBlank(); + } + + private final class BrowseAction extends AbstractAction + { + BrowseAction() + { + this.putValue(Action.NAME, "Browse"); + this.putValue( + Action.SHORT_DESCRIPTION, + "Browse the file-system for a configuration file"); + this.putValue( + Action.LONG_DESCRIPTION, + "Browse the file-system for a configuration file"); + } + + @Override + public void actionPerformed(final ActionEvent e) + { + Optional fileLocationPath; + try + { + fileLocationPath = Optional.ofNullable(TPCPLocationPanel.this.getAbsoluteFileLocationPath()); + } + catch(final Exception ex) + { + fileLocationPath = Optional.empty(); + } + final VirtualFile toSelect = fileLocationPath + .map(LocalFileSystem.getInstance()::findFileByNioFile) + .orElseGet(() -> ProjectUtil.guessProjectDir(TPCPLocationPanel.this.project)); + + final VirtualFile chosen = FileChooser.chooseFile( + chooserDescriptor(), + TPCPLocationPanel.this, + TPCPLocationPanel.this.project, + toSelect); + if(chosen != null) + { + final File newConfigFile = VfsUtilCore.virtualToIoFile(chosen); + TPCPLocationPanel.this.txtFileLocation.setText(newConfigFile.getAbsolutePath()); + } + } + + private static FileChooserDescriptor chooserDescriptor() + { + return new FileChooserDescriptor(true, false, true, true, false, false) + .withExtensionFilter("jar"); + } + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java new file mode 100644 index 0000000..ff93160 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; + +import java.awt.BorderLayout; + +import javax.swing.JPanel; + +import com.intellij.openapi.project.Project; +import com.intellij.ui.TitledSeparator; +import com.intellij.ui.ToolbarDecorator; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.ui.config.project.PMDConfigPanel; +import software.xdev.pmd.ui.config.project.components.shared.LocationPanelManager; + + +public class TPCPLocationPanelManager + extends LocationPanelManager +{ + private static final int TYPE_COL_MIN_WIDTH = 50; + private static final int TYPE_COL_MAX_WIDTH = 100; + + public TPCPLocationPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) + { + super(project, pmdConfigPanel, new TPCPLocationTableModel(), TPCPLocationDialog::new); + } + + @Override + public JPanel panel() + { + this.setColumnWith(this.locationTable, 0, TYPE_COL_MIN_WIDTH, TYPE_COL_MAX_WIDTH, TYPE_COL_MAX_WIDTH); + this.configureLocationTableDefaults(); + + final ToolbarDecorator tableDecorator = ToolbarDecorator.createDecorator(this.locationTable) + .setAddAction(new AddLocationAction()) + .setRemoveAction(new RemoveLocationAction()) + .setMoveDownAction(e -> this.tryMove(1)) + .setMoveUpAction(e -> this.tryMove(-1)) + .setPreferredSize(DECORATOR_DIMENSIONS); + + final JPanel container = new JPanel(new BorderLayout()); + container.add(new TitledSeparator("Third-Party Rules"), BorderLayout.NORTH); + container.add(tableDecorator.createPanel(), BorderLayout.CENTER); + return container; + } + + private void tryMove(final int direction) + { + final int selectedRowIndex = this.locationTable.getSelectedRow(); + this.locationModel.trySwap(selectedRowIndex, selectedRowIndex + direction); + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java new file mode 100644 index 0000000..773dd06 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java @@ -0,0 +1,65 @@ +package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; + +import java.util.Collections; + +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.ui.config.project.components.shared.LocationTableModel; + + +public class TPCPLocationTableModel extends LocationTableModel +{ + private static final int COLUMN_TYPE = 0; + private static final int COLUMN_LOCATION = 1; + private static final int NUMBER_OF_COLUMNS = 2; + + public void trySwap(final int index, final int otherIndex) + { + try + { + Collections.swap(this.locations, index, otherIndex); + } + catch(final IndexOutOfBoundsException ignored) + { + } + } + + @Override + public int getColumnCount() + { + return NUMBER_OF_COLUMNS; + } + + @Override + public Class getColumnClass(final int columnIndex) + { + return String.class; + } + + @Override + public String getColumnName(final int column) + { + return switch(column) + { + case COLUMN_TYPE -> "Type"; + case COLUMN_LOCATION -> "Location"; + default -> "???"; + }; + } + + @Override + public int getRowCount() + { + return this.locations.size(); + } + + @Override + public Object getValueAt(final int rowIndex, final int columnIndex) + { + return switch(columnIndex) + { + case COLUMN_TYPE -> this.locations.get(rowIndex).type().name(); + case COLUMN_LOCATION -> this.locations.get(rowIndex).displayLocation(); + default -> throw new IllegalArgumentException("Invalid column: " + columnIndex); + }; + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java deleted file mode 100644 index 3522dbe..0000000 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/ThirdPartyClasspathPanelManager.java +++ /dev/null @@ -1,141 +0,0 @@ -package software.xdev.pmd.ui.config.project.components.thirdpartyclasspath; - -import java.awt.BorderLayout; -import java.util.List; -import java.util.stream.IntStream; - -import javax.swing.DefaultListModel; -import javax.swing.JList; -import javax.swing.JPanel; - -import com.intellij.openapi.fileChooser.FileChooser; -import com.intellij.openapi.fileChooser.FileChooserDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectUtil; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VfsUtilCore; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.ui.AnActionButton; -import com.intellij.ui.AnActionButtonRunnable; -import com.intellij.ui.TitledSeparator; -import com.intellij.ui.ToolbarDecorator; -import com.intellij.ui.components.JBList; - -import software.xdev.pmd.ui.config.project.PMDConfigPanel; -import software.xdev.pmd.ui.config.project.components.SubPMDConfigPanelManager; - - -public class ThirdPartyClasspathPanelManager extends SubPMDConfigPanelManager -{ - private final DefaultListModel pathListModel = new DefaultListModel<>(); - private final JList pathList = new JBList<>(this.pathListModel); - - public ThirdPartyClasspathPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) - { - super(project, pmdConfigPanel); - } - - public JPanel buildPanel() - { - final JPanel container = new JPanel(new BorderLayout()); - container.add( - new TitledSeparator("Third-Party Rules"), - BorderLayout.NORTH); - container.add(ToolbarDecorator.createDecorator(this.pathList) - .setAddAction(new AddPathAction()) - .setEditAction(new EditPathAction()) - .setRemoveAction(new RemovePathAction()) - .setPreferredSize(DECORATOR_DIMENSIONS) - .createPanel(), - BorderLayout.CENTER); - return container; - } - - public List getThirdPartyClassPath() - { - return IntStream.range(0, this.pathListModel.size()) - .mapToObj(this.pathListModel::get) - .toList(); - } - - public void setThirdPartyClassPath(final List classPath) - { - this.pathListModel.clear(); - this.pathListModel.addAll(classPath); - } - - private static FileChooserDescriptor chooserDescriptor() - { - return new FileChooserDescriptor(true, false, true, true, false, false) - .withFileFilter(file -> { - final String currentExtension = file.getExtension(); - return currentExtension != null && "jar".equalsIgnoreCase(currentExtension.trim()); - }); - } - - class AddPathAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final VirtualFile chosen = FileChooser.chooseFile( - ThirdPartyClasspathPanelManager.chooserDescriptor(), - ThirdPartyClasspathPanelManager.this.pmdConfigPanel, - ThirdPartyClasspathPanelManager.this.project, - ProjectUtil.guessProjectDir(ThirdPartyClasspathPanelManager.this.project)); - if(chosen != null) - { - ThirdPartyClasspathPanelManager.this.pathListModel.addElement( - VfsUtilCore.virtualToIoFile(chosen).getAbsolutePath()); - } - } - } - - - class EditPathAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final int selected = ThirdPartyClasspathPanelManager.this.pathList.getSelectedIndex(); - if(selected < 0) - { - return; - } - - final DefaultListModel listModel = ThirdPartyClasspathPanelManager.this.pathListModel; - final String selectedFile = listModel.get(selected); - - final VirtualFile toSelect = LocalFileSystem.getInstance().findFileByPath(selectedFile); - final VirtualFile chosen = FileChooser.chooseFile( - ThirdPartyClasspathPanelManager.chooserDescriptor(), - ThirdPartyClasspathPanelManager.this.project, - toSelect); - if(chosen != null) - { - listModel.remove(selected); - listModel.add(selected, VfsUtilCore.virtualToIoFile(chosen).getAbsolutePath()); - ThirdPartyClasspathPanelManager.this.pathList.setSelectedIndex(selected); - } - } - } - - - class RemovePathAction implements AnActionButtonRunnable - { - @Override - public void run(final AnActionButton anActionButton) - { - final int[] selected = ThirdPartyClasspathPanelManager.this.pathList.getSelectedIndices(); - if(selected == null || selected.length == 0) - { - return; - } - - for(final int index : selected) - { - ThirdPartyClasspathPanelManager.this.pathListModel.remove(index); - } - } - } -} diff --git a/src/main/java/software/xdev/pmd/util/io/FilePaths.java b/src/main/java/software/xdev/pmd/util/io/FilePaths.java deleted file mode 100644 index 27088ee..0000000 --- a/src/main/java/software/xdev/pmd/util/io/FilePaths.java +++ /dev/null @@ -1,135 +0,0 @@ -package software.xdev.pmd.util.io; - -import java.io.File; -import java.util.regex.Pattern; - -import org.apache.commons.io.FilenameUtils; -import org.jetbrains.annotations.NotNull; - - -public final class FilePaths -{ - /** - * Get the relative path from one file to another, specifying the directory separator. If one of the provided - * resources does not exist, it is assumed to be a file unless it ends with '/' or '\'. - *

- * This (and the tests) is from ... - all credit to the author. - * Ideally we'd use URI.relativize(), but it only works in the prefixes match (...). - * - * @param targetPath targetPath is calculated to this file - * @param basePath basePath is calculated from this file - * @param pathSeparator directory separator. The platform default is not assumed so that we can test Unix behaviour - * when running on Windows (for example) - * @return the relative file. - * @see "http://stackoverflow.com/a/3054692" - */ - @SuppressWarnings("PMD.AvoidStringBuilderOrBuffer") - public static String relativePath( - @NotNull final String targetPath, - @NotNull final String basePath, - @NotNull final String pathSeparator) - { - // Normalize the paths - String normalizedTargetPath = FilenameUtils.normalizeNoEndSeparator(targetPath); - String normalizedBasePath = FilenameUtils.normalizeNoEndSeparator(basePath); - - // Undo the changes to the separators made by normalization - if("/".equals(pathSeparator)) - { - normalizedTargetPath = FilenameUtils.separatorsToUnix(normalizedTargetPath); - normalizedBasePath = FilenameUtils.separatorsToUnix(normalizedBasePath); - } - else if("\\".equals(pathSeparator)) - { - normalizedTargetPath = FilenameUtils.separatorsToWindows(normalizedTargetPath); - normalizedBasePath = FilenameUtils.separatorsToWindows(normalizedBasePath); - } - else - { - throw new IllegalArgumentException("Unrecognised dir separator '" + pathSeparator + "'"); - } - - final String[] base = normalizedBasePath.split(Pattern.quote(pathSeparator)); - final String[] target = normalizedTargetPath.split(Pattern.quote(pathSeparator)); - - // First get all the common elements. Store them as a string, - // and also count how many of them there are. - final StringBuilder common = new StringBuilder(500); - - int commonIndex = 0; - while(commonIndex < target.length && commonIndex < base.length - && target[commonIndex].equals(base[commonIndex])) - { - common.append(target[commonIndex]).append(pathSeparator); - commonIndex++; - } - - if(commonIndex == 0) - { - // No single common path element. This most - // likely indicates differing drive letters, like C: and D:. - // These paths cannot be relativized. - throw new PathResolutionException("No common path element found for '" + normalizedTargetPath - + "' and '" + normalizedBasePath + "'"); - } - - // The number of directories we have to backtrack depends on whether the base is a file or a dir - // For example, the relative path from - // - // /foo/bar/baz/gg/ff to /foo/bar/baz - // - // ".." if ff is a file - // "../.." if ff is a directory - // - // The following is a heuristic to figure out if the base refers to a file or dir. It's not perfect, because - // the resource referred to by this path may not actually exist, but it's the best I can do - final boolean baseIsFile = isBaseFile(basePath, pathSeparator, normalizedBasePath); - - final StringBuilder relative = new StringBuilder(100); - - if(base.length != commonIndex) - { - final int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex; - - for(int i = 0; i < numDirsUp; i++) - { - relative.append("..").append(pathSeparator); - } - } - - if(common.length() >= normalizedTargetPath.length()) - { - return "."; - } - - relative.append(normalizedTargetPath.substring(common.length())); - return relative.toString(); - } - - private static boolean isBaseFile( - @NotNull final String basePath, - @NotNull final String pathSeparator, - final String normalizedBasePath) - { - final File baseResource = new File(normalizedBasePath); - if(baseResource.exists()) - { - return baseResource.isFile(); - } - - return !basePath.endsWith(pathSeparator); - } - - public static class PathResolutionException extends RuntimeException - { - PathResolutionException(final String msg) - { - super(msg); - } - } - - private FilePaths() - { - } -} diff --git a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java index 3166b9a..2466ef0 100644 --- a/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java +++ b/src/main/java/software/xdev/pmd/util/io/ProjectFilePaths.java @@ -1,6 +1,7 @@ package software.xdev.pmd.util.io; import java.io.File; +import java.util.Objects; import org.jetbrains.annotations.NotNull; @@ -29,10 +30,7 @@ private ProjectFilePaths( public String makeProjectRelativePathAbsolute(final String path) { - if(path == null || this.project.isDefault()) - { - throw new IllegalArgumentException("Invalid path or project"); - } + Objects.requireNonNull(path, "Invalid path"); final VirtualFile projectDir = ProjectUtil.guessProjectDir(this.project); if(projectDir == null) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 57240ee..691fbdc 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -31,6 +31,12 @@ + + + From 8374747636ed9b6112f14fcd5c4b5ffa4667719b Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 10:52:55 +0200 Subject: [PATCH 17/30] Fix serialization problem --- .../state/project/ThirdPartyCPState.java | 130 ++++++++++-------- 1 file changed, 75 insertions(+), 55 deletions(-) diff --git a/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java b/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java index 0621daa..02a170a 100644 --- a/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java +++ b/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java @@ -1,9 +1,9 @@ package software.xdev.pmd.config.state.project; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -35,7 +35,7 @@ public class ThirdPartyCPState private static final Logger LOG = Logger.getInstance(ThirdPartyCPState.class); @XCollection - LinkedHashSet activeIds; + List activeIds; @MapAnnotation List maven; @@ -52,6 +52,18 @@ public ThirdPartyCPState() // for serialization } + public ThirdPartyCPState( + final List activeIds, + final List maven, + final List absoluteFile, + final List relativeFile) + { + this.activeIds = activeIds; + this.maven = maven; + this.absoluteFile = absoluteFile; + this.relativeFile = relativeFile; + } + public static ThirdPartyCPState create( final List thirdPartyCPLocations) { @@ -60,34 +72,31 @@ public static ThirdPartyCPState create( return null; } - final ThirdPartyCPState state = new ThirdPartyCPState(); - state.activeIds = thirdPartyCPLocations.stream() - .map(ThirdPartyCPLocation::id) - .collect(Collectors.toCollection(LinkedHashSet::new)); - final Map, List> groupedByClazz = thirdPartyCPLocations.stream().collect(Collectors.groupingBy(ThirdPartyCPLocation::getClass)); - state.maven = create( - groupedByClazz, - MavenThirdPartyCPLocation.class, - loc -> new MavenThirdPartyCPLocationState( - loc.id(), - loc.mavenId().groupId(), - loc.mavenId().artifactId(), - loc.mavenId().version())); - - state.absoluteFile = create( - groupedByClazz, - AbsoluteFileThirdPartyCPLocation.class, - loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())); - - state.relativeFile = create( - groupedByClazz, - RelativeFileThirdPartyCPLocation.class, - loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())); - - return state; + return new ThirdPartyCPState( + thirdPartyCPLocations.stream() + .map(ThirdPartyCPLocation::id) + .distinct() + .toList(), + create( + groupedByClazz, + MavenThirdPartyCPLocation.class, + loc -> new MavenThirdPartyCPLocationState( + loc.id(), + loc.mavenId().groupId(), + loc.mavenId().artifactId(), + loc.mavenId().version())), + create( + groupedByClazz, + AbsoluteFileThirdPartyCPLocation.class, + loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())), + create( + groupedByClazz, + RelativeFileThirdPartyCPLocation.class, + loc -> new FileThirdPartyCPLocationState(loc.id(), loc.location())) + ); } @Nullable @@ -114,10 +123,19 @@ List create( public List populate(@NotNull final Project project) { + if(this.activeIds == null || this.activeIds.isEmpty()) + { + return List.of(); + } + + final Set activeIdsFastAccess = Set.copyOf(this.activeIds); + final Map availableLocations = Stream.of( - this.populateLocations(MavenThirdPartyCPLocationFactory.class, project, this.maven), - this.populateLocations(AbsoluteFileThirdPartyCPLocationFactory.class, project, this.absoluteFile), - this.populateLocations(RelativeFileThirdPartyCPLocationFactory.class, project, this.relativeFile)) + new LocationPopulator<>(MavenThirdPartyCPLocationFactory.class, this.maven), + new LocationPopulator<>(AbsoluteFileThirdPartyCPLocationFactory.class, this.absoluteFile), + new LocationPopulator<>(RelativeFileThirdPartyCPLocationFactory.class, this.relativeFile)) + .map(p -> + p.populate(activeIdsFastAccess, project)) .flatMap(List::stream) .collect(Collectors.toMap(ThirdPartyCPLocation::id, Function.identity())); @@ -126,35 +144,37 @@ public List populate(@NotNull final Project project) .toList(); } - private < + public record LocationPopulator< L extends ThirdPartyCPLocation, S extends ThirdPartyCPLocationState, - F extends ThirdPartyCPLocationFactory> - List populateLocations( - final Class factoryClazz, - final Project project, - final List persistedStates - ) + F extends ThirdPartyCPLocationFactory>( + Class factoryClazz, + List persistedStates) { - if(persistedStates == null || persistedStates.isEmpty()) + public List populate( + final Set activeIds, + final Project project) { - return List.of(); + if(this.persistedStates == null || this.persistedStates.isEmpty()) + { + return List.of(); + } + final F factory = project.getService(this.factoryClazz); + return this.persistedStates.stream() + .filter(s -> activeIds.contains(s.id())) + .map(s -> { + try + { + return (ThirdPartyCPLocation)factory.fromPersisted(s); + } + catch(final Exception ex) + { + LOG.error("Encountered problem while populating location[id=" + s.id() + "]", ex); + return null; + } + }) + .filter(Objects::nonNull) + .toList(); } - final F factory = project.getService(factoryClazz); - return persistedStates.stream() - .filter(s -> this.activeIds.contains(s.id())) - .map(s -> { - try - { - return (ThirdPartyCPLocation)factory.fromPersisted(s); - } - catch(final Exception ex) - { - LOG.error("Encountered problem while populating location[id=" + s.id() + "]", ex); - return null; - } - }) - .filter(Objects::nonNull) - .toList(); } } From f39281b68b63430379b19b5a7deb6aba0cecdca8 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 10:54:32 +0200 Subject: [PATCH 18/30] Fix CS and PMD --- .../rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java | 2 +- .../components/thirdpartyclasspath/TPCPLocationTableModel.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java index cb64215..d40ce0a 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java @@ -107,7 +107,7 @@ protected void logImpl(final Level level, final String message) // noop } - public void addErrorsAsSuppressedAndClear(final Exception ex) + void addErrorsAsSuppressedAndClear(final Exception ex) { this.errors.forEach(ex::addSuppressed); this.errors.clear(); diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java index 773dd06..f121b39 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java @@ -20,6 +20,7 @@ public void trySwap(final int index, final int otherIndex) } catch(final IndexOutOfBoundsException ignored) { + // Ignore } } From 7789279f008e0aa0b3ca676d92e236ff57137729 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 14:16:46 +0200 Subject: [PATCH 19/30] Add support for using thrid party rules from maven central --- .../ApplicationConfigurationState.java | 51 +++++ .../maven/MavenId.java | 2 +- .../ideamaven/IDEAMavenMirrorUrlResolver.java | 178 ++++++++++++++++ .../PMDMavenAfterImportConfigurator.java | 4 +- .../maven/resolve/MavenArtifactResolver.java | 198 ++++++++++++++++++ .../mirror/MavenMirrorUrlResolver.java | 18 ++ .../mirror/MavenMirrorUrlResolverService.java | 42 ++++ .../maven/MavenThirdPartyCPLocation.java | 1 + .../MavenThirdPartyCPLocationFactory.java | 5 +- .../PMDApplicationConfigurable.java | 100 +++++++++ .../ui/config/project/PMDConfigurable.java | 12 -- .../components/shared/LocationDialog.java | 1 + .../TPCPLocationPanel.java | 4 +- .../TPCPLocationPanelManager.java | 6 +- .../TPCPLocationTableModel.java | 5 +- src/main/resources/META-INF/plugin-maven.xml | 6 +- src/main/resources/META-INF/plugin.xml | 13 ++ 17 files changed, 623 insertions(+), 23 deletions(-) create mode 100644 src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java rename src/main/java/software/xdev/pmd/{model/config/thirdpartycplocation => }/maven/MavenId.java (93%) create mode 100644 src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java rename src/main/java/software/xdev/pmd/maven/{ => ideamaven}/PMDMavenAfterImportConfigurator.java (99%) create mode 100644 src/main/java/software/xdev/pmd/maven/resolve/MavenArtifactResolver.java create mode 100644 src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java create mode 100644 src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java create mode 100644 src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java diff --git a/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java new file mode 100644 index 0000000..0659e08 --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.config.state.application; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.RoamingType; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.annotations.Tag; + + +@State( + name = "PMD-X-Application", + storages = {@Storage(value = "pmd-x-app.xml", roamingType = RoamingType.DISABLED)} +) +public class ApplicationConfigurationState + implements PersistentStateComponent +{ + private ApplicationSettings applicationSettings = new ApplicationSettings(); + + @Nullable + public String getArtifactRepositoryBaseUrlOverride() + { + return this.applicationSettings.artifactRepositoryBaseUrlOverride; + } + + public void setArtifactRepositoryBaseUrlOverride(@Nullable final String artifactRepositoryBaseUrlOverride) + { + this.applicationSettings.artifactRepositoryBaseUrlOverride = artifactRepositoryBaseUrlOverride; + } + + @Override + @NotNull + public ApplicationSettings getState() + { + return this.applicationSettings; + } + + @Override + public void loadState(@NotNull final ApplicationSettings sourceApplicationSettings) + { + this.applicationSettings = sourceApplicationSettings; + } + + public static class ApplicationSettings + { + @Tag + String artifactRepositoryBaseUrlOverride; + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java b/src/main/java/software/xdev/pmd/maven/MavenId.java similarity index 93% rename from src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java rename to src/main/java/software/xdev/pmd/maven/MavenId.java index 9202d13..2722a2f 100644 --- a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenId.java +++ b/src/main/java/software/xdev/pmd/maven/MavenId.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.model.config.thirdpartycplocation.maven; +package software.xdev.pmd.maven; import java.nio.CharBuffer; import java.util.Objects; diff --git a/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java new file mode 100644 index 0000000..69d70c1 --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java @@ -0,0 +1,178 @@ +package software.xdev.pmd.maven.ideamaven; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jdom.Namespace; +import org.jetbrains.idea.maven.utils.MavenEelUtil; +import org.jspecify.annotations.Nullable; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.JDOMUtil; +import com.intellij.openapi.util.text.StringUtil; + +import software.xdev.pmd.maven.resolve.mirror.MavenMirrorUrlResolver; + + +public class IDEAMavenMirrorUrlResolver implements MavenMirrorUrlResolver +{ + @Override + public Optional resolve(final Project project) + { + return Optional.ofNullable(MavenEelUtil.resolveUserSettingsPathBlocking(null, project)) + // Somehow some static methods of MavenUtil were converted to instance methods + // in https://github.com/JetBrains/intellij-community/commit/cb47b90276da8ac0e9d51b635ee44596f42711f2 + // therefore making them no longer accessible because the class is now private + .map(path -> getMirroredUrl(path, DEFAULT_CENTRAL_REPOSITORY_URL, "central")) + .filter(s -> !DEFAULT_CENTRAL_REPOSITORY_URL.equals(s)); + } + + // region FromIDEA + + private static final List SETTINGS_LIST_NAMESPACES = List.of( + "http://maven.apache.org/SETTINGS/1.0.0", + "http://maven.apache.org/SETTINGS/1.1.0", + "http://maven.apache.org/SETTINGS/1.2.0" + ); + + @SuppressWarnings("java:S135") + public static String getMirroredUrl(final Path settingsFile, final String url, final String id) + { + try + { + final Element mirrorParent = getElementWithRegardToNamespace( + getDomRootElement(settingsFile), "mirrors", SETTINGS_LIST_NAMESPACES); + if(mirrorParent == null) + { + return url; + } + + final List mirrors = + getElementsWithRegardToNamespace(mirrorParent, "mirror", SETTINGS_LIST_NAMESPACES); + for(final Element el : mirrors) + { + final Element mirrorOfElement = getElementWithRegardToNamespace( + el, "mirrorOf", SETTINGS_LIST_NAMESPACES); + final Element mirrorUrlElement = getElementWithRegardToNamespace( + el, "url", SETTINGS_LIST_NAMESPACES); + if(mirrorOfElement == null) + { + continue; + } + if(mirrorUrlElement == null) + { + continue; + } + + final String mirrorOf = mirrorOfElement.getTextTrim(); + final String mirrorUrl = mirrorUrlElement.getTextTrim(); + + if(StringUtil.isEmptyOrSpaces(mirrorOf) || StringUtil.isEmptyOrSpaces(mirrorUrl)) + { + continue; + } + + if(isMirrorApplicable(mirrorOf, url, id)) + { + return mirrorUrl; + } + } + } + catch(final Exception ignore) + { + // ignored + } + + return url; + } + + private static Element getDomRootElement(final Path file) throws IOException, JDOMException + { + return JDOMUtil.load(new InputStreamReader(Files.newInputStream(file), StandardCharsets.UTF_8)); + } + + private static @Nullable Element getElementWithRegardToNamespace( + final Element parent, + final String childName, + final List namespaces) + { + Element element = parent.getChild(childName); + if(element != null) + { + return element; + } + for(final String namespace : namespaces) + { + element = parent.getChild(childName, Namespace.getNamespace(namespace)); + if(element != null) + { + return element; + } + } + return null; + } + + private static List getElementsWithRegardToNamespace( + final Element parent, + final String childrenName, + final List namespaces) + { + List elements = parent.getChildren(childrenName); + if(!elements.isEmpty()) + { + return elements; + } + for(final String namespace : namespaces) + { + elements = parent.getChildren(childrenName, Namespace.getNamespace(namespace)); + if(!elements.isEmpty()) + { + return elements; + } + } + return List.of(); + } + + @SuppressWarnings("PMD.AvoidUsingHardCodedIP") + private static boolean isMirrorApplicable(final String mirrorOf, final String url, final String id) + { + final Set patterns = new HashSet<>(StringUtil.split(mirrorOf, ",")); + + if(patterns.contains("!" + id)) + { + return false; + } + + if(patterns.contains("*") || patterns.contains(id)) + { + return true; + } + if(patterns.contains("external:*")) + { + try + { + final URI uri = URI.create(url); + return !"file".equals(uri.getScheme()) + && !"localhost".equals(uri.getHost()) + && !"127.0.0.1".equals(uri.getHost()); + } + catch(final IllegalArgumentException e) + { + return false; + } + } + return false; + } + + // endregion +} diff --git a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java b/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java similarity index 99% rename from src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java rename to src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java index af89ec4..cec9e40 100644 --- a/src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java +++ b/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java @@ -1,4 +1,4 @@ -package software.xdev.pmd.maven; +package software.xdev.pmd.maven.ideamaven; import java.nio.file.Paths; import java.util.ArrayList; @@ -103,6 +103,8 @@ private void configureFromMaven( this.configureLocations(project, configElement, currentConfig, builder); this.configureExclusions(configElement, builder); + + // TODO Configure dependencies } private void configureScanScope(final Element configElement, final PluginConfigurationBuilder builder) diff --git a/src/main/java/software/xdev/pmd/maven/resolve/MavenArtifactResolver.java b/src/main/java/software/xdev/pmd/maven/resolve/MavenArtifactResolver.java new file mode 100644 index 0000000..f1aa3ef --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/resolve/MavenArtifactResolver.java @@ -0,0 +1,198 @@ +package software.xdev.pmd.maven.resolve; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.config.state.application.ApplicationConfigurationState; +import software.xdev.pmd.maven.MavenId; +import software.xdev.pmd.maven.resolve.mirror.MavenMirrorUrlResolver; +import software.xdev.pmd.maven.resolve.mirror.MavenMirrorUrlResolverService; + + +public class MavenArtifactResolver implements Disposable +{ + private static final Logger LOG = Logger.getInstance(MavenArtifactResolver.class); + + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DOWNLOAD_TIMEOUT = Duration.ofMinutes(3); + + private static final int DOWNLOAD_TRIES = 2; + + private final HttpClient httpClient; + private final List>> mavenBaseUrlSupplier; + private final Supplier m2RootSupplier; + + public MavenArtifactResolver(final Project project) + { + this.httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(CONNECT_TIMEOUT) + .build(); + + this.mavenBaseUrlSupplier = List.of( + Suppliers.memoize(() -> project.getService(MavenMirrorUrlResolverService.class).resolve(project)), + () -> Optional.of(MavenMirrorUrlResolver.DEFAULT_CENTRAL_REPOSITORY_URL) + ); + + this.m2RootSupplier = Suppliers.memoize(MavenArtifactResolver::determineM2Root); + } + + public Path ensureResolved(final MavenId mavenId) + { + final String groupIdWithSlash = mavenId.groupId().replace('.', '/'); + final String jarFileName = mavenId.artifactId() + "-" + mavenId.version() + ".jar"; + final Path resolvedPath = this.m2RootSupplier.get() + .resolve(groupIdWithSlash) + .resolve(mavenId.artifactId()) + .resolve(mavenId.version()) + .resolve(jarFileName); + + if(!Files.exists(resolvedPath)) + { + this.download(mavenId, groupIdWithSlash, jarFileName, resolvedPath); + } + + return resolvedPath; + } + + private void download( + final MavenId mavenId, + final String groupIdWithSlash, + final String jarFileName, + final Path resolvedPath) + { + final List downloadExceptions = new ArrayList<>(); + if(this.baseUrlsForDownload().stream() + .map(Supplier::get) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .noneMatch(baseUrl -> { + final String downloadUrl = baseUrl + + (baseUrl.endsWith("/") ? "" : "/") + + groupIdWithSlash + + "/" + mavenId.artifactId() + + "/" + mavenId.version() + + "/" + jarFileName; + for(int i = 1; i <= DOWNLOAD_TRIES; i++) + { + try + { + Files.createDirectories(resolvedPath.getParent()); + this.downloadTo(downloadUrl, resolvedPath); + return true; + } + catch(final Exception e) + { + LOG.debug( + "Download attempt #" + i + " " + mavenId + " from " + downloadUrl + " failed", + e); + downloadExceptions.add(e); + } + + try + { + Thread.sleep(1000); + } + catch(final InterruptedException e) + { + Thread.currentThread().interrupt(); + } + } + return false; + })) + { + final IllegalStateException ex = new IllegalStateException("Failed to download " + mavenId); + downloadExceptions.forEach(ex::addSuppressed); + throw ex; + } + } + + private List>> baseUrlsForDownload() + { + return Optional.ofNullable(ApplicationManager.getApplication()) + .map(application -> application.getService(ApplicationConfigurationState.class)) + .map(ApplicationConfigurationState::getArtifactRepositoryBaseUrlOverride) + .filter(url -> !url.isBlank()) + .filter(url -> { + try + { + final String scheme = new URI(url).getScheme(); + return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme); + } + catch(final URISyntaxException ignored) + { + return false; + } + }) + .map(url -> List.>>of(() -> Optional.of(url))) + .orElse(this.mavenBaseUrlSupplier); + } + + private void downloadTo(final String url, final Path target) throws IOException + { + LOG.debug("Downloading " + url + " to " + target); + + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(DOWNLOAD_TIMEOUT) + .GET() + .build(); + final Path tmp = Files.createTempFile(target.getParent(), ".download-", ".part"); + try + { + final HttpResponse response = this.httpClient.send( + request, + HttpResponse.BodyHandlers.ofFile( + tmp)); + if(response.statusCode() != 200) + { + throw new IOException("Encountered HTTP " + response.statusCode() + " while downloading " + url); + } + Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } + catch(final InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new IOException("Download interrupted: " + url, e); + } + finally + { + Files.deleteIfExists(tmp); + } + } + + private static Path determineM2Root() + { + final String envMavenUserHome = System.getenv("MAVEN_USER_HOME"); + if(envMavenUserHome != null) + { + return Path.of(envMavenUserHome).resolve("repository"); + } + + return Path.of(System.getProperty("user.home"), ".m2", "repository"); + } + + @Override + public void dispose() + { + this.httpClient.close(); + } +} diff --git a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java new file mode 100644 index 0000000..dae8c0a --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java @@ -0,0 +1,18 @@ +package software.xdev.pmd.maven.resolve.mirror; + +import java.util.Optional; + +import com.intellij.openapi.project.Project; + + +public interface MavenMirrorUrlResolver +{ + String DEFAULT_CENTRAL_REPOSITORY_URL = "https://repo.maven.apache.org/maven2/"; + + default int order() + { + return 1000; + } + + Optional resolve(Project project); +} diff --git a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java new file mode 100644 index 0000000..93fff06 --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java @@ -0,0 +1,42 @@ +package software.xdev.pmd.maven.resolve.mirror; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; + + +public class MavenMirrorUrlResolverService +{ + private final ExtensionPointName epMirrorUrlResolver = + ExtensionPointName.create("software.xdev.pmd.mavenMirrorUrlResolver"); + + private List lastSeenExtensions; + private List cachedUrlResolvers; + + private List orderedLangResolvers() + { + final List extensions = this.epMirrorUrlResolver.getExtensionList(); + if(this.cachedUrlResolvers == null || extensions != this.lastSeenExtensions) + { + this.cachedUrlResolvers = extensions + .stream() + .sorted(Comparator.comparingInt(MavenMirrorUrlResolver::order)) + .toList(); + this.lastSeenExtensions = extensions; + } + return this.cachedUrlResolvers; + } + + public Optional resolve(final Project project) + { + return this.orderedLangResolvers() + .stream() + .map(r -> r.resolve(project)) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .findFirst(); + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java index e83a2dd..d825fb4 100644 --- a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java @@ -3,6 +3,7 @@ import java.net.URL; import java.util.Objects; +import software.xdev.pmd.maven.MavenId; import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationType; diff --git a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java index fbba0f4..288fc04 100644 --- a/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java @@ -5,6 +5,8 @@ import com.intellij.openapi.project.Project; import software.xdev.pmd.config.state.project.thirdpartycp.MavenThirdPartyCPLocationState; +import software.xdev.pmd.maven.MavenId; +import software.xdev.pmd.maven.resolve.MavenArtifactResolver; import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocationFactory; @@ -45,7 +47,6 @@ public MavenThirdPartyCPLocation fromUI(final MavenId uiState) protected Path resolveMavenArtifactId(final MavenId mavenId) { - // TODO - return null; + return this.project.getService(MavenArtifactResolver.class).ensureResolved(mavenId); } } diff --git a/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java new file mode 100644 index 0000000..72de955 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java @@ -0,0 +1,100 @@ +package software.xdev.pmd.ui.config.application; + +import java.util.Objects; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.UIManager; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.options.Configurable; +import com.intellij.util.ui.FormBuilder; + +import software.xdev.pmd.config.state.application.ApplicationConfigurationState; + + +public class PMDApplicationConfigurable implements Configurable +{ + private final ApplicationConfigurationState applicationConfigurationState; + private JTextField txtArtifactRepositoryBaseUrlOverride; + + public PMDApplicationConfigurable() + { + this(ApplicationManager.getApplication().getService(ApplicationConfigurationState.class)); + } + + PMDApplicationConfigurable(@NotNull final ApplicationConfigurationState applicationConfigurationState) + { + this.applicationConfigurationState = applicationConfigurationState; + } + + @Override + public String getDisplayName() + { + return "PMD Global Settings"; + } + + @Override + public JComponent createComponent() + { + this.txtArtifactRepositoryBaseUrlOverride = new JTextField(); + + this.reset(); + + final JTextArea taDesc = new JTextArea(""" + This URL will be used to download maven artifact versions instead of https://repo.maven.apache.org/maven2/. + It takes precedence over a mirror auto-detected from Mavens settings.xml. + Only needed if no usable settings.xml is present or a setup that is not detected properly. + """); + taDesc.setFont(UIManager.getFont("Label.font")); + taDesc.setEditable(false); + taDesc.setOpaque(false); + taDesc.setWrapStyleWord(true); + taDesc.setLineWrap(true); + + return FormBuilder.createFormBuilder() + .addComponent(taDesc) + .addLabeledComponent( + "Artifact download mirror override:", + this.txtArtifactRepositoryBaseUrlOverride) + .addComponentFillVertically(new JPanel(), 0) + .getPanel(); + } + + @Override + public boolean isModified() + { + return !Objects.equals( + normalise(this.txtArtifactRepositoryBaseUrlOverride.getText()), + this.applicationConfigurationState.getArtifactRepositoryBaseUrlOverride()); + } + + @Override + public void apply() + { + this.applicationConfigurationState.setArtifactRepositoryBaseUrlOverride( + normalise(this.txtArtifactRepositoryBaseUrlOverride.getText())); + } + + @Override + public void reset() + { + this.txtArtifactRepositoryBaseUrlOverride.setText( + Objects.requireNonNullElse(this.applicationConfigurationState.getArtifactRepositoryBaseUrlOverride(), "")); + } + + @Nullable + private static String normalise(@Nullable final String value) + { + if(value == null || value.isBlank()) + { + return null; + } + return value.trim(); + } +} diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java index da14443..fe116cf 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java @@ -36,12 +36,6 @@ public String getDisplayName() return "PMD"; } - @Override - public String getHelpTopic() - { - return null; - } - @Override public JComponent createComponent() { @@ -92,10 +86,4 @@ public void reset() this.configPanel.showPluginConfiguration(this.pluginConfigurationManager.getCurrent()); } - - @Override - public void disposeUIResources() - { - // do nothing - } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java index c73689d..e942578 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java @@ -215,6 +215,7 @@ protected void onNext() final L location = this.getLocationFromPanelAndValidate(this.locationPanel); if(location == null) { + this.commitButton.setEnabled(true); return; } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java index 11ad912..23fa987 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanel.java @@ -32,10 +32,10 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.JBUI; +import software.xdev.pmd.maven.MavenId; import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; import software.xdev.pmd.model.config.thirdpartycplocation.file.absolute.AbsoluteFileThirdPartyCPLocationFactory; import software.xdev.pmd.model.config.thirdpartycplocation.file.relative.RelativeFileThirdPartyCPLocationFactory; -import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenId; import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenThirdPartyCPLocationFactory; @@ -177,7 +177,7 @@ else if(this.txtMavenArtifactGroupId.isEnabled() && this.isNotBlank(this.txtMavenArtifactArtifactId.getText()) && this.isNotBlank(this.txtMavenArtifactVersion.getText())) { - this.project.getService(MavenThirdPartyCPLocationFactory.class) + return this.project.getService(MavenThirdPartyCPLocationFactory.class) .fromUI(new MavenId( this.txtMavenArtifactGroupId.getText(), this.txtMavenArtifactArtifactId.getText(), diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java index ff93160..77d25d0 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java @@ -17,7 +17,7 @@ public class TPCPLocationPanelManager extends LocationPanelManager { private static final int TYPE_COL_MIN_WIDTH = 50; - private static final int TYPE_COL_MAX_WIDTH = 100; + private static final int TYPE_COL_MAX_WIDTH = 150; public TPCPLocationPanelManager(final Project project, final PMDConfigPanel pmdConfigPanel) { @@ -46,6 +46,8 @@ public JPanel panel() private void tryMove(final int direction) { final int selectedRowIndex = this.locationTable.getSelectedRow(); - this.locationModel.trySwap(selectedRowIndex, selectedRowIndex + direction); + final int otherIndex = selectedRowIndex + direction; + this.locationModel.trySwap(selectedRowIndex, otherIndex); + this.locationTable.setRowSelectionInterval(otherIndex, otherIndex); } } diff --git a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java index f121b39..d71800c 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java @@ -12,15 +12,16 @@ public class TPCPLocationTableModel extends LocationTableModel - + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 691fbdc..00d3e0f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -38,16 +38,25 @@ + + + + + + @@ -136,5 +145,9 @@ name="languageVersionResolver" interface="software.xdev.pmd.langversion.LanguageVersionResolver" dynamic="true"/> + From 3c83e653700318b428e5efc5e6e02a5e39e0aa97 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 14:37:35 +0200 Subject: [PATCH 20/30] Update docs --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c628b..e7ae769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ # 2.0.0 +* Add support 3rd Party Rules #10 + * Allows to use custom PMD rules shipped in JAR files + * JARs can be located from project-relative files, absolute files or maven artifacts + * Maven artifacts + * Are downloaded and loaded automatically if not present + * The download tries to use the configured Maven mirror or can be completely overwritten in the settings * Make it possible to add (project-wide) exclusions #58 * Add support for importing settings from Maven PMD plugin (best-effort) * Add shortcuts for jumping to previous/next result/violation @@ -6,7 +12,6 @@ * Only show "Enable automatic build" hint on startup when projects have PMD enabled * Refactor location store * Order bundled locations at the end -* Implement support for 3rd party class path rules # 1.1.4 * Prevent rare NPE in module configuration UI #107 From 971a2d327d5c5273a53b99d77e20135e98e442e5 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 14:38:00 +0200 Subject: [PATCH 21/30] Improve maven import to now also import maven 3rd party locations --- .../PMDMavenAfterImportConfigurator.java | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java b/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java index cec9e40..c20c8ee 100644 --- a/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java +++ b/src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java @@ -23,9 +23,8 @@ import org.jetbrains.idea.maven.model.MavenPlugin; import org.jetbrains.idea.maven.model.MavenProfile; import org.jetbrains.idea.maven.project.MavenProject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import fleet.util.GlobKt; @@ -36,13 +35,15 @@ import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfigurationLocation; import software.xdev.pmd.model.config.rulesetlocation.file.FileConfigurationLocation; import software.xdev.pmd.model.config.rulesetlocation.file.RelativeFileConfigurationLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.maven.MavenThirdPartyCPLocationFactory; import software.xdev.pmd.model.scope.ScanScope; @SuppressWarnings("UnstableApiUsage") public class PMDMavenAfterImportConfigurator implements MavenAfterImportConfigurator { - private static final Logger LOG = LoggerFactory.getLogger(PMDMavenAfterImportConfigurator.class); + private static final Logger LOG = Logger.getInstance(PMDMavenAfterImportConfigurator.class); private static final List PMD_PLUGIN_MAVEN_IDS = List.of( new MavenId("org.apache.maven.plugins", "maven-pmd-plugin", null)); @@ -79,7 +80,7 @@ public void afterImport(final MavenAfterImportConfigurator.Context context) final PluginConfigurationBuilder builder = new PluginConfigurationBuilder(currentConfig); - this.configureFromMaven(project, configElement, currentConfig, builder); + this.configureFromMaven(project, mavenPlugin, configElement, currentConfig, builder); final PluginConfiguration newConfig = builder.build(); if(currentConfig.isIdentical(newConfig)) @@ -94,6 +95,7 @@ public void afterImport(final MavenAfterImportConfigurator.Context context) private void configureFromMaven( final Project project, + final MavenPlugin mavenPlugin, final Element configElement, final PluginConfiguration currentConfig, final PluginConfigurationBuilder builder) @@ -104,7 +106,7 @@ private void configureFromMaven( this.configureExclusions(configElement, builder); - // TODO Configure dependencies + this.configureThirdPartyCPLocations(project, mavenPlugin, builder); } private void configureScanScope(final Element configElement, final PluginConfigurationBuilder builder) @@ -162,6 +164,29 @@ private void configureLocations( .withActiveLocationIds(new TreeSet<>(mavenLocations.stream().map(ConfigurationLocation::getId).toList())); } + private ConfigurationLocation createFileBasedConfigurationLocation(final Project project, final String s) + { + final boolean absolutePath; + try + { + absolutePath = Paths.get(s).isAbsolute(); + } + catch(final Exception ex) + { + // Ignore invalid paths + return null; + } + + final String id = String.valueOf(s.hashCode()); + final ConfigurationLocation configurationLocation = absolutePath + ? new FileConfigurationLocation(project, id) + : new RelativeFileConfigurationLocation(project, id); + configurationLocation.setLocation(s); + configurationLocation.setDescription(s); + + return configurationLocation; + } + @SuppressWarnings("PMD.AvoidStringBuilderOrBuffer") private void configureExclusions(final Element configElement, final PluginConfigurationBuilder builder) { @@ -183,27 +208,43 @@ private void configureExclusions(final Element configElement, final PluginConfig .toList()); } - private ConfigurationLocation createFileBasedConfigurationLocation(final Project project, final String s) + private void configureThirdPartyCPLocations( + final Project project, + final MavenPlugin mavenPlugin, + final PluginConfigurationBuilder builder) { - final boolean absolutePath; - try - { - absolutePath = Paths.get(s).isAbsolute(); - } - catch(final Exception ex) + final List relevantDeps = mavenPlugin.getDependencies() + .stream() + // These modules are already bundled + .filter(dep -> !"net.sourceforge.pmd".equals(dep.getGroupId())) + .toList(); + if(relevantDeps.isEmpty()) { - // Ignore invalid paths - return null; + builder.withThirdPartyCPLocations(List.of()); + return; } - final String id = String.valueOf(s.hashCode()); - final ConfigurationLocation configurationLocation = absolutePath - ? new FileConfigurationLocation(project, id) - : new RelativeFileConfigurationLocation(project, id); - configurationLocation.setLocation(s); - configurationLocation.setDescription(s); - - return configurationLocation; + final MavenThirdPartyCPLocationFactory factory = project.getService(MavenThirdPartyCPLocationFactory.class); + builder.withThirdPartyCPLocations( + relevantDeps.stream() + .map(id -> new software.xdev.pmd.maven.MavenId( + id.getGroupId(), + id.getArtifactId(), + id.getVersion())) + .map(id -> { + try + { + return factory.fromUI(id); + } + catch(final Exception ex) + { + LOG.debug("Maven 3rd party CP import of " + id + " failed", ex); + return null; + } + }) + .filter(Objects::nonNull) + .map(ThirdPartyCPLocation.class::cast) + .toList()); } record MavenProjectAndPlugin( From f3fd88e23462ffd4ff8185f8d5100852d993e56d Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 31 Jul 2026 14:41:15 +0200 Subject: [PATCH 22/30] Cleanup comment --- .../xdev/pmd/config/state/module/ModuleConfigurationState.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java index a7b06f0..b9836c8 100644 --- a/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java +++ b/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java @@ -24,9 +24,6 @@ import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; -/** - * A manager for CheckStyle module configuration. - */ @State( name = ModuleConfigurationState.ID_MODULE_PLUGIN, storages = {@Storage(StoragePathMacros.MODULE_FILE)} From 402b1b992748dc72e5bce9979478d90c80e50f06 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 10:50:24 +0200 Subject: [PATCH 23/30] Link intellij bug report See #116 --- .../xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java index 69d70c1..42ef76b 100644 --- a/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java +++ b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java @@ -33,6 +33,7 @@ public Optional resolve(final Project project) // Somehow some static methods of MavenUtil were converted to instance methods // in https://github.com/JetBrains/intellij-community/commit/cb47b90276da8ac0e9d51b635ee44596f42711f2 // therefore making them no longer accessible because the class is now private + // Problem reported in https://youtrack.jetbrains.com/issue/IDEA-392283 .map(path -> getMirroredUrl(path, DEFAULT_CENTRAL_REPOSITORY_URL, "central")) .filter(s -> !DEFAULT_CENTRAL_REPOSITORY_URL.equals(s)); } From 48293dac03193e312d250d42755eedc77c1c3156 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 10:59:23 +0200 Subject: [PATCH 24/30] Cleanup --- .../state/application/ApplicationConfigurationState.java | 2 +- .../ui/config/application/PMDApplicationConfigurable.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java index 0659e08..2effe7f 100644 --- a/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java +++ b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java @@ -12,7 +12,7 @@ @State( name = "PMD-X-Application", - storages = {@Storage(value = "pmd-x-app.xml", roamingType = RoamingType.DISABLED)} + storages = {@Storage(value = "pmd-x-app.xml", roamingType = RoamingType.LOCAL)} ) public class ApplicationConfigurationState implements PersistentStateComponent diff --git a/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java index 72de955..80297c2 100644 --- a/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java +++ b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java @@ -47,8 +47,8 @@ public JComponent createComponent() this.reset(); final JTextArea taDesc = new JTextArea(""" - This URL will be used to download maven artifact versions instead of https://repo.maven.apache.org/maven2/. - It takes precedence over a mirror auto-detected from Mavens settings.xml. + This URL will be used to download maven artifacts instead of https://repo.maven.apache.org/maven2/. + It takes precedence over a mirror auto-detected from settings.xml. Only needed if no usable settings.xml is present or a setup that is not detected properly. """); taDesc.setFont(UIManager.getFont("Label.font")); @@ -60,7 +60,7 @@ public JComponent createComponent() return FormBuilder.createFormBuilder() .addComponent(taDesc) .addLabeledComponent( - "Artifact download mirror override:", + "Artifact download override:", this.txtArtifactRepositoryBaseUrlOverride) .addComponentFillVertically(new JPanel(), 0) .getPanel(); From a2b3c43606638307eadfd28c84149b730f4d274a Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 11:12:02 +0200 Subject: [PATCH 25/30] Abstract EP resolution code --- .../pmd/langversion/LanguageResolver.java | 8 ++--- .../langversion/LanguageVersionResolver.java | 8 ++--- .../LanguageVersionResolverService.java | 26 +++----------- .../mirror/MavenMirrorUrlResolver.java | 9 ++--- .../mirror/MavenMirrorUrlResolverService.java | 28 +++------------ .../CachedOrderedExtensionPointContainer.java | 34 +++++++++++++++++++ .../software/xdev/pmd/util/ep/HasOrder.java | 9 +++++ 7 files changed, 60 insertions(+), 62 deletions(-) create mode 100644 src/main/java/software/xdev/pmd/util/ep/CachedOrderedExtensionPointContainer.java create mode 100644 src/main/java/software/xdev/pmd/util/ep/HasOrder.java diff --git a/src/main/java/software/xdev/pmd/langversion/LanguageResolver.java b/src/main/java/software/xdev/pmd/langversion/LanguageResolver.java index 2cb559f..ebf7cb0 100644 --- a/src/main/java/software/xdev/pmd/langversion/LanguageResolver.java +++ b/src/main/java/software/xdev/pmd/langversion/LanguageResolver.java @@ -6,15 +6,11 @@ import com.intellij.psi.PsiFile; import net.sourceforge.pmd.lang.Language; +import software.xdev.pmd.util.ep.HasOrder; -public interface LanguageResolver +public interface LanguageResolver extends HasOrder { - default int order() - { - return 1000; - } - boolean isFileSupported(@NotNull PsiFile file); @Nullable diff --git a/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolver.java b/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolver.java index d1bc11d..fd728af 100644 --- a/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolver.java +++ b/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolver.java @@ -9,15 +9,11 @@ import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageVersion; +import software.xdev.pmd.util.ep.HasOrder; -public interface LanguageVersionResolver +public interface LanguageVersionResolver extends HasOrder { - default int order() - { - return 1000; - } - @NotNull Set supportedLanguages(); diff --git a/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolverService.java b/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolverService.java index 4887395..e8f0d83 100644 --- a/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolverService.java +++ b/src/main/java/software/xdev/pmd/langversion/LanguageVersionResolverService.java @@ -18,36 +18,20 @@ import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageVersion; +import software.xdev.pmd.util.ep.CachedOrderedExtensionPointContainer; public class LanguageVersionResolverService { - private final ExtensionPointName epLang = - ExtensionPointName.create("software.xdev.pmd.languageResolver"); + private final CachedOrderedExtensionPointContainer langContainer + = new CachedOrderedExtensionPointContainer<>("languageResolver"); private final ExtensionPointName epVersion = ExtensionPointName.create("software.xdev.pmd.languageVersionResolver"); - private List lastSeenLangExtensions; - private List cachedOrderedLangResolvers; - private List lastSeenVersionExtensions; private Map> cachedOrderedVersionResolvers; - private List orderedLangResolvers() - { - final List extensions = this.epLang.getExtensionList(); - if(this.cachedOrderedLangResolvers == null || extensions != this.lastSeenLangExtensions) - { - this.cachedOrderedLangResolvers = extensions - .stream() - .sorted(Comparator.comparingInt(LanguageResolver::order)) - .toList(); - this.lastSeenLangExtensions = extensions; - } - return this.cachedOrderedLangResolvers; - } - private Map> orderedVersionResolvers() { final List extensions = this.epVersion.getExtensionList(); @@ -74,7 +58,7 @@ private Map> orderedVersionResolvers() public Optional resolveLanguage(@NotNull final PsiFile file) { - return this.orderedLangResolvers() + return this.langContainer.orderedEps() .stream() .map(r -> r.resolveLanguage(file)) .filter(Objects::nonNull) @@ -108,6 +92,6 @@ public Set supportedLanguageIds() public boolean isFileSupportedByAnyResolver(final PsiFile file) { - return this.orderedLangResolvers().stream().anyMatch(r -> r.isFileSupported(file)); + return this.langContainer.orderedEps().stream().anyMatch(r -> r.isFileSupported(file)); } } diff --git a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java index dae8c0a..478a83a 100644 --- a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java @@ -4,15 +4,12 @@ import com.intellij.openapi.project.Project; +import software.xdev.pmd.util.ep.HasOrder; -public interface MavenMirrorUrlResolver + +public interface MavenMirrorUrlResolver extends HasOrder { String DEFAULT_CENTRAL_REPOSITORY_URL = "https://repo.maven.apache.org/maven2/"; - default int order() - { - return 1000; - } - Optional resolve(Project project); } diff --git a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java index 93fff06..ea15587 100644 --- a/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java @@ -1,38 +1,20 @@ package software.xdev.pmd.maven.resolve.mirror; -import java.util.Comparator; -import java.util.List; import java.util.Optional; -import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import software.xdev.pmd.util.ep.CachedOrderedExtensionPointContainer; + public class MavenMirrorUrlResolverService { - private final ExtensionPointName epMirrorUrlResolver = - ExtensionPointName.create("software.xdev.pmd.mavenMirrorUrlResolver"); - - private List lastSeenExtensions; - private List cachedUrlResolvers; - - private List orderedLangResolvers() - { - final List extensions = this.epMirrorUrlResolver.getExtensionList(); - if(this.cachedUrlResolvers == null || extensions != this.lastSeenExtensions) - { - this.cachedUrlResolvers = extensions - .stream() - .sorted(Comparator.comparingInt(MavenMirrorUrlResolver::order)) - .toList(); - this.lastSeenExtensions = extensions; - } - return this.cachedUrlResolvers; - } + private final CachedOrderedExtensionPointContainer container = + new CachedOrderedExtensionPointContainer<>("mavenMirrorUrlResolver"); public Optional resolve(final Project project) { - return this.orderedLangResolvers() + return this.container.orderedEps() .stream() .map(r -> r.resolve(project)) .filter(Optional::isPresent) diff --git a/src/main/java/software/xdev/pmd/util/ep/CachedOrderedExtensionPointContainer.java b/src/main/java/software/xdev/pmd/util/ep/CachedOrderedExtensionPointContainer.java new file mode 100644 index 0000000..1c2835b --- /dev/null +++ b/src/main/java/software/xdev/pmd/util/ep/CachedOrderedExtensionPointContainer.java @@ -0,0 +1,34 @@ +package software.xdev.pmd.util.ep; + +import java.util.Comparator; +import java.util.List; + +import com.intellij.openapi.extensions.ExtensionPointName; + + +public class CachedOrderedExtensionPointContainer +{ + private final ExtensionPointName epn; + + private List lastSeen; + private List cachedOrdered; + + public CachedOrderedExtensionPointContainer(final String suffix) + { + this.epn = ExtensionPointName.create("software.xdev.pmd." + suffix); + } + + public List orderedEps() + { + final List extensions = this.epn.getExtensionList(); + if(this.cachedOrdered == null || extensions != this.lastSeen) + { + this.cachedOrdered = extensions + .stream() + .sorted(Comparator.comparingInt(HasOrder::order)) + .toList(); + this.lastSeen = extensions; + } + return this.cachedOrdered; + } +} diff --git a/src/main/java/software/xdev/pmd/util/ep/HasOrder.java b/src/main/java/software/xdev/pmd/util/ep/HasOrder.java new file mode 100644 index 0000000..c08da05 --- /dev/null +++ b/src/main/java/software/xdev/pmd/util/ep/HasOrder.java @@ -0,0 +1,9 @@ +package software.xdev.pmd.util.ep; + +public interface HasOrder +{ + default int order() + { + return 1000; + } +} From 73f441908cadf33689b75b6d6ce2f6e7a37277a7 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 11:23:58 +0200 Subject: [PATCH 26/30] Cleanup UI --- .../ApplicationConfigurationState.java | 3 +- .../PMDApplicationConfigurable.java | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java index 2effe7f..be222ce 100644 --- a/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java +++ b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java @@ -4,7 +4,6 @@ import org.jetbrains.annotations.Nullable; import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.RoamingType; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.util.xmlb.annotations.Tag; @@ -12,7 +11,7 @@ @State( name = "PMD-X-Application", - storages = {@Storage(value = "pmd-x-app.xml", roamingType = RoamingType.LOCAL)} + storages = {@Storage(value = "pmd-x-app.xml")} ) public class ApplicationConfigurationState implements PersistentStateComponent diff --git a/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java index 80297c2..8cf140e 100644 --- a/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java +++ b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java @@ -46,19 +46,12 @@ public JComponent createComponent() this.reset(); - final JTextArea taDesc = new JTextArea(""" - This URL will be used to download maven artifacts instead of https://repo.maven.apache.org/maven2/. - It takes precedence over a mirror auto-detected from settings.xml. - Only needed if no usable settings.xml is present or a setup that is not detected properly. - """); - taDesc.setFont(UIManager.getFont("Label.font")); - taDesc.setEditable(false); - taDesc.setOpaque(false); - taDesc.setWrapStyleWord(true); - taDesc.setLineWrap(true); - return FormBuilder.createFormBuilder() - .addComponent(taDesc) + .addComponent(this.createInfoBoxTa(""" + This URL will be used to download maven artifacts instead of https://repo.maven.apache.org/maven2/. + It takes precedence over a mirror auto-detected from settings.xml. + Only needed if no usable settings.xml is present or a setup that is not detected properly. + """)) .addLabeledComponent( "Artifact download override:", this.txtArtifactRepositoryBaseUrlOverride) @@ -66,6 +59,17 @@ public JComponent createComponent() .getPanel(); } + private JTextArea createInfoBoxTa(final String text) + { + final JTextArea ta = new JTextArea(text); + ta.setFont(UIManager.getFont("Label.font")); + ta.setEditable(false); + ta.setOpaque(false); + ta.setWrapStyleWord(true); + ta.setLineWrap(true); + return ta; + } + @Override public boolean isModified() { From 450921f899c58fb60f4fae2208f2c14315628071 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 11:36:14 +0200 Subject: [PATCH 27/30] More comment cleanup --- .../rulesetlocation/file/FileConfigurationLocation.java | 5 +---- .../file/RelativeFileConfigurationLocation.java | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java index 86dac58..d082aa3 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java @@ -20,9 +20,6 @@ import software.xdev.pmd.util.io.ProjectFilePaths; -/** - * A configuration file on a mounted file system. - */ public class FileConfigurationLocation extends ConfigurationLocation { protected long nextReloadRuleSetMs; @@ -103,7 +100,7 @@ protected synchronized RuleSet loadRuleSet(final ClassLoader classLoader) throws { this.nextReloadRuleSetMs = System.currentTimeMillis() + 10 * 1000; - // Do this here due to IOEx + // Do this here due to IOEx in Lambda final String rulesetXmlContent = new String(Files.readAllBytes(this.locationPath)); final RuleSet ruleSet = DefaultRuleSetLoaderCreator.createAndLoad(rsl -> LoadFromStringRuleSetLoaderWorkaround.loadFromString( diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java index e4cc727..2e1ccaf 100644 --- a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java @@ -8,10 +8,6 @@ import software.xdev.pmd.model.config.rulesetlocation.ConfigurationType; -/** - * A configuration file on a mounted file system which will always be referred to by a path relative to the project - * path. - */ public class RelativeFileConfigurationLocation extends FileConfigurationLocation { private static final String LEGACY_IDEA_PROJECT_DIR = "$PROJECT_DIR$"; From 77d316f1d67601a28aaea20e42e924402fc0f9d6 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 11:48:12 +0200 Subject: [PATCH 28/30] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7eef1ac..4ef8755 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ A plugin for IntelliJ that provides code analysis and highlighting with
* Currently Java and Kotlin are supported, Language versions are automatically detected +* Include / exclude files with regex support +* Use JARs for custom 3rd party rules + * Can be automatically downloaded from Maven +* Configuration import from Maven projects (best-effort) ## Usage 1. Install the plugin and open a project From 51f4fdf5212e8b23d28c168280ff231f90304d2c Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 12:36:37 +0200 Subject: [PATCH 29/30] Fine tune settings UI --- .../xdev/pmd/ui/config/project/PMDConfigPanel.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java index 945a353..fb02994 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java +++ b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigPanel.java @@ -125,21 +125,21 @@ private JPanel buildConfigPanel() this.rsLocationPanelManager.panel(), 3, 1.0, - 300); + 250); this.addPanel( configFilePanel, this.exclusionPanelManager.getPanel(), 4, 0.1, - 150); + 130); this.addPanel( configFilePanel, this.tpcpLocationPanelManager.panel(), 5, - 0.5, - 150); + 0.1, + 140); return configFilePanel; } From d486c23428f7b24568a2f65719c7500e64f40100 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 3 Aug 2026 12:36:44 +0200 Subject: [PATCH 30/30] Adjust readme and docs --- README.md | 6 +++--- assets/custom-rule.avif | Bin 0 -> 15930 bytes assets/project-configuration.avif | Bin 17781 -> 20737 bytes 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 assets/custom-rule.avif diff --git a/README.md b/README.md index 4ef8755..23569b0 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ A plugin for IntelliJ that provides code analysis and highlighting with

* Currently Java and Kotlin are supported, Language versions are automatically detected * Include / exclude files with regex support -* Use JARs for custom 3rd party rules - * Can be automatically downloaded from Maven -* Configuration import from Maven projects (best-effort) +* Use JARs for custom 3rd party rules
+ * JARs can automatically be downloaded from Maven Central +* Configuration can be imported from Maven projects (best-effort) ## Usage 1. Install the plugin and open a project diff --git a/assets/custom-rule.avif b/assets/custom-rule.avif new file mode 100644 index 0000000000000000000000000000000000000000..a57b4746ed320004243363f687ca9fdad5cc364a GIT binary patch literal 15930 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
#**97{l`4@WEV7+n^GbvKjb-$m*P6I+%(vb5#vh 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 K)oPzAu_|-j{Z&N( delta 17659 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