diff --git a/CHANGELOG.md b/CHANGELOG.md index add7a17..e7ae769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ -# 1.2.0 +# 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 * 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/README.md b/README.md index 7eef1ac..23569b0 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
+ * 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 0000000..a57b474 Binary files /dev/null and b/assets/custom-rule.avif differ diff --git a/assets/project-configuration.avif b/assets/project-configuration.avif index 1bc195c..0a5c3ba 100644 Binary files a/assets/project-configuration.avif and b/assets/project-configuration.avif differ 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 diff --git a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java index e68bbf3..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 @@ -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..28f9301 --- /dev/null +++ b/src/main/java/software/xdev/pmd/analysis/ProjectScanClasspathManager.java @@ -0,0 +1,70 @@ +package software.xdev.pmd.analysis; + +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.model.config.thirdpartycplocation.ThirdPartyCPLocation; + + +public class ProjectScanClasspathManager implements Disposable +{ + private static final ClassLoader DEFAULT_CL = ProjectScanClasspathManager.class.getClassLoader(); + private static final Logger LOG = Logger.getInstance(ProjectScanClasspathManager.class); + + private List cachedKey; + private URLClassLoader classLoader; + + public void configure(final List locations) + { + if(!Objects.equals(this.cachedKey, locations)) + { + this.closeCurrentClassLoader(); + if(!locations.isEmpty()) + { + this.classLoader = new URLClassLoader( + locations.stream().map(ThirdPartyCPLocation::url).toArray(URL[]::new), + DEFAULT_CL); + } + + this.cachedKey = locations; + } + } + + 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/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 d7f9e76..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; @@ -11,7 +12,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.config.plugin.PatternContainer; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; +import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation; import software.xdev.pmd.model.scope.ScanScope; @@ -27,6 +30,7 @@ public record PluginConfiguration( SortedSet projectRelativeFileExclusions, SortedSet locations, SortedSet activeLocationIds, + 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 2950f25..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; @@ -12,9 +13,11 @@ import com.intellij.openapi.project.Project; -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.config.plugin.PatternContainer; +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; @@ -27,6 +30,7 @@ public final class PluginConfigurationBuilder private SortedSet projectRelativeFileExclusions; private SortedSet locations; private SortedSet activeLocationIds; + private List thirdPartyCPLocations; private boolean importSettingsFromMaven; public PluginConfigurationBuilder(final Project project) @@ -34,12 +38,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.thirdPartyCPLocations = null; this.importSettingsFromMaven = false; } @@ -52,6 +57,7 @@ public PluginConfigurationBuilder(final PluginConfiguration copyFrom) this.projectRelativeFileExclusions = copyFrom.projectRelativeFileExclusions(); this.locations = copyFrom.locations(); this.activeLocationIds = copyFrom.activeLocationIds(); + this.thirdPartyCPLocations = copyFrom.thirdPartyCPLocations(); this.importSettingsFromMaven = copyFrom.importSettingsFromMaven(); } @@ -87,6 +93,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 +131,9 @@ public PluginConfigurationBuilder withLocations(@NotNull final SortedSet thirdPartyCPLocations) { - this.scanScope = newScanScope; + this.thirdPartyCPLocations = thirdPartyCPLocations; return this; } @@ -147,6 +159,7 @@ public PluginConfiguration build() .filter(Objects::nonNull) .collect(Collectors.toCollection(TreeSet::new)) : new TreeSet<>(), + 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 b0495db..967ff34 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.thirdPartyCPLocations()); } private ProjectConfigurationState projectConfigurationState() 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/config/state/application/ApplicationConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java new file mode 100644 index 0000000..be222ce --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/application/ApplicationConfigurationState.java @@ -0,0 +1,50 @@ +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.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")} +) +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/config/state/module/ModuleConfigurationState.java b/src/main/java/software/xdev/pmd/config/state/module/ModuleConfigurationState.java index 2c0496b..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 @@ -21,12 +21,9 @@ 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; -/** - * A manager for CheckStyle module configuration. - */ @State( name = ModuleConfigurationState.ID_MODULE_PLUGIN, storages = {@Storage(StoragePathMacros.MODULE_FILE)} 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..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 @@ -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,13 +21,13 @@ 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.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.config.plugin.PatternContainer; +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; @@ -50,6 +52,8 @@ public class ProjectSettingsState List activeLocationIds; @MapAnnotation List locations; + @MapAnnotation + ThirdPartyCPState thirdPartyCPState; @Tag boolean importSettingsFromMaven; @@ -63,23 +67,43 @@ 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.thirdPartyCPState = ThirdPartyCPState.create(currentConfig.thirdPartyCPLocations()); projectSettings.importSettingsFromMaven = currentConfig.importSettingsFromMaven(); return projectSettings; } + public static > C useNullIfEmpty(final C inputs) + { + return inputs.isEmpty() ? null : inputs; + } + + public static , R> R useNullIfEmpty( + final C inputs, + final Function mapperIfPresent) + { + if(inputs.isEmpty()) + { + return null; + } + + return mapperIfPresent.apply(inputs); + } + @SuppressWarnings("unused") public ProjectSettingsState() { @@ -100,6 +124,9 @@ public PluginConfigurationBuilder populate( .withActiveLocationIds(new TreeSet<>(requireNonNullElseGet( this.activeLocationIds, ArrayList::new))) + .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..02a170a --- /dev/null +++ b/src/main/java/software/xdev/pmd/config/state/project/ThirdPartyCPState.java @@ -0,0 +1,180 @@ +package software.xdev.pmd.config.state.project; + +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; + +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 + List activeIds; + + @MapAnnotation + List maven; + + @MapAnnotation + List absoluteFile; + + @MapAnnotation + List relativeFile; + + @SuppressWarnings("unused") + 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) + { + if(thirdPartyCPLocations.isEmpty()) + { + return null; + } + + final Map, List> groupedByClazz = + thirdPartyCPLocations.stream().collect(Collectors.groupingBy(ThirdPartyCPLocation::getClass)); + + 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 + 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) + { + if(this.activeIds == null || this.activeIds.isEmpty()) + { + return List.of(); + } + + final Set activeIdsFastAccess = Set.copyOf(this.activeIds); + + final Map availableLocations = Stream.of( + 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())); + + return this.activeIds.stream() + .map(availableLocations::get) + .toList(); + } + + public record LocationPopulator< + L extends ThirdPartyCPLocation, + S extends ThirdPartyCPLocationState, + F extends ThirdPartyCPLocationFactory>( + Class factoryClazz, + List persistedStates) + { + public List populate( + final Set activeIds, + final Project project) + { + 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(); + } + } +} 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/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/MavenId.java b/src/main/java/software/xdev/pmd/maven/MavenId.java new file mode 100644 index 0000000..2722a2f --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/MavenId.java @@ -0,0 +1,51 @@ +package software.xdev.pmd.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/maven/ideamaven/IDEAMavenMirrorUrlResolver.java b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java new file mode 100644 index 0000000..42ef76b --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/ideamaven/IDEAMavenMirrorUrlResolver.java @@ -0,0 +1,179 @@ +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 + // 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)); + } + + // 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 82% rename from src/main/java/software/xdev/pmd/maven/PMDMavenAfterImportConfigurator.java rename to src/main/java/software/xdev/pmd/maven/ideamaven/PMDMavenAfterImportConfigurator.java index e3aa8df..c20c8ee 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; @@ -23,26 +23,27 @@ 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; 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.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) @@ -103,6 +105,8 @@ private void configureFromMaven( this.configureLocations(project, configElement, currentConfig, builder); this.configureExclusions(configElement, builder); + + this.configureThirdPartyCPLocations(project, mavenPlugin, builder); } private void configureScanScope(final Element configElement, final PluginConfigurationBuilder builder) @@ -160,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) { @@ -181,29 +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(absolutePath - ? s - : "$PROJECT_DIR$/" + 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( 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..478a83a --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolver.java @@ -0,0 +1,15 @@ +package software.xdev.pmd.maven.resolve.mirror; + +import java.util.Optional; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.util.ep.HasOrder; + + +public interface MavenMirrorUrlResolver extends HasOrder +{ + String DEFAULT_CENTRAL_REPOSITORY_URL = "https://repo.maven.apache.org/maven2/"; + + 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..ea15587 --- /dev/null +++ b/src/main/java/software/xdev/pmd/maven/resolve/mirror/MavenMirrorUrlResolverService.java @@ -0,0 +1,24 @@ +package software.xdev.pmd.maven.resolve.mirror; + +import java.util.Optional; + +import com.intellij.openapi.project.Project; + +import software.xdev.pmd.util.ep.CachedOrderedExtensionPointContainer; + + +public class MavenMirrorUrlResolverService +{ + private final CachedOrderedExtensionPointContainer container = + new CachedOrderedExtensionPointContainer<>("mavenMirrorUrlResolver"); + + public Optional resolve(final Project project) + { + return this.container.orderedEps() + .stream() + .map(r -> r.resolve(project)) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .findFirst(); + } +} 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 deleted file mode 100644 index fa7ed22..0000000 --- a/src/main/java/software/xdev/pmd/model/config/file/FileConfigurationLocation.java +++ /dev/null @@ -1,107 +0,0 @@ -package software.xdev.pmd.model.config.file; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Instant; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -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.util.io.ProjectFilePaths; - - -/** - * A configuration file on a mounted file system. - */ -public class FileConfigurationLocation extends ConfigurationLocation -{ - private long nextReloadRuleSetMs; - private Instant lastModifiedFileTime; - - public FileConfigurationLocation( - @NotNull final Project project, - @NotNull final String id) - { - this(project, id, ConfigurationType.LOCAL_FILE); - } - - public FileConfigurationLocation( - @NotNull final Project project, - @NotNull final String id, - @NotNull final ConfigurationType configurationType) - { - super(id, configurationType, project); - } - - @Override - public String getLocation() - { - return this.projectFilePaths().detokenize(super.getLocation()); - } - - @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)); - } - - protected Path getLocationPath() - { - return Paths.get(this.getLocation()); - } - - @Nullable - protected Instant lastModifiedTimeFromLocation() - { - try - { - return Files.getLastModifiedTime(this.getLocationPath()).toInstant(); - } - catch(final IOException e) - { - return null; - } - } - - @SuppressWarnings("checkstyle:IllegalIdentifierName") - @Override - protected synchronized RuleSet loadRuleSet() throws IOException - { - this.nextReloadRuleSetMs = System.currentTimeMillis() + 10 * 1000; - - final RuleSet ruleSet = new RuleSetLoader().loadFromString( - this.getLocation(), - new String(Files.readAllBytes(this.getLocationPath()))); - this.lastModifiedFileTime = this.lastModifiedTimeFromLocation(); - return ruleSet; - } - - @Override - protected boolean shouldReloadRuleSet() - { - // Check if recently checked - return System.currentTimeMillis() > this.nextReloadRuleSetMs - // Check if file was modified - && (this.lastModifiedFileTime == null - || !this.lastModifiedFileTime.equals(this.lastModifiedTimeFromLocation())); - } - - @NotNull - protected ProjectFilePaths projectFilePaths() - { - return this.getProject().getService(ProjectFilePaths.class); - } -} 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 deleted file mode 100644 index 1301734..0000000 --- a/src/main/java/software/xdev/pmd/model/config/file/RelativeFileConfigurationLocation.java +++ /dev/null @@ -1,41 +0,0 @@ -package software.xdev.pmd.model.config.file; - -import org.jetbrains.annotations.NotNull; - -import com.intellij.openapi.project.Project; - -import software.xdev.pmd.model.config.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 -{ - public RelativeFileConfigurationLocation( - @NotNull final Project project, - @NotNull final String id) - { - super(project, id, ConfigurationType.PROJECT_RELATIVE); - } - - @Override - public boolean canBeResolvedInDefaultProject() - { - return false; - } - - @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( - this.projectFilePaths().makeProjectRelative( - this.projectFilePaths().detokenize(location)))); - } -} 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 68% 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 83f5f19..59af910 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; /** @@ -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/ConfigurationType.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/ConfigurationType.java similarity index 64% 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..dc2cfe4 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,10 +1,10 @@ -package software.xdev.pmd.model.config; +package software.xdev.pmd.model.config.rulesetlocation; 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, @@ -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); } } 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 84% 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 7441232..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,8 +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.ConfigurationType; +import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation; 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/rulesetlocation/bundled/BundledConfigurationLocation.java similarity index 62% 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 097fcd8..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 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.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 @@ -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,30 +36,43 @@ 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 - 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.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/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 72% 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 44cdce3..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; @@ -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/rulesetlocation/file/FileConfigurationLocation.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java new file mode 100644 index 0000000..d082aa3 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/FileConfigurationLocation.java @@ -0,0 +1,132 @@ +package software.xdev.pmd.model.config.rulesetlocation.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; +import java.time.Instant; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.openapi.project.Project; + +import net.sourceforge.pmd.lang.rule.RuleSet; +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; + + +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; + protected String description; + + public FileConfigurationLocation( + @NotNull final Project project, + @NotNull final String id) + { + this(project, id, ConfigurationType.LOCAL_FILE); + } + + public FileConfigurationLocation( + @NotNull final Project project, + @NotNull final String id, + @NotNull final ConfigurationType configurationType) + { + super(id, configurationType, project); + } + + @Override + public String getLocation() + { + return this.location; + } + + @Override + public void setLocation(final String location) + { + this.location = location; + this.locationPath = this.getLocationPath(); + } + + protected String getRealLocation() + { + return this.projectFilePaths().toSystemPath(this.getLocation()); + } + + protected Path getLocationPath() + { + return Paths.get(this.getRealLocation()); + } + + @Override + public void setDescription(final String description) + { + this.description = description; + } + + @Override + public String getDescription() + { + return this.description; + } + + @Nullable + protected Instant lastModifiedTimeFromLocation() + { + try + { + return Files.getLastModifiedTime(this.locationPath).toInstant(); + } + catch(final IOException e) + { + return null; + } + } + + @SuppressWarnings("checkstyle:IllegalIdentifierName") + @Override + protected synchronized RuleSet loadRuleSet(final ClassLoader classLoader) throws IOException + { + this.nextReloadRuleSetMs = System.currentTimeMillis() + 10 * 1000; + + // 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( + rsl.loadResourcesWith(classLoader), + this.getLocation(), + rulesetXmlContent)); + this.lastModifiedFileTime = this.lastModifiedTimeFromLocation(); + this.previouslyUsedClassLoaderRef = new WeakReference<>(classLoader); + return ruleSet; + } + + @Override + protected boolean shouldReloadRuleSet(final ClassLoader classLoader) + { + // 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())); + } + + @NotNull + protected ProjectFilePaths projectFilePaths() + { + return this.getProject().getService(ProjectFilePaths.class); + } +} 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 new file mode 100644 index 0000000..2e1ccaf --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/RelativeFileConfigurationLocation.java @@ -0,0 +1,50 @@ +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.rulesetlocation.ConfigurationType; + + +public class RelativeFileConfigurationLocation extends FileConfigurationLocation +{ + private static final String LEGACY_IDEA_PROJECT_DIR = "$PROJECT_DIR$"; + + public RelativeFileConfigurationLocation( + @NotNull final Project project, + @NotNull final String id) + { + super(project, id, ConfigurationType.PROJECT_RELATIVE); + } + + @SuppressWarnings("checkstyle:FinalParameters") + @Override + public void setLocation(String location) + { + // 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 + protected String getRealLocation() + { + return this.projectFilePaths().makeProjectRelativePathAbsolute(super.getRealLocation()); + } +} 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 new file mode 100644 index 0000000..d40ce0a --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreator.java @@ -0,0 +1,116 @@ +package software.xdev.pmd.model.config.rulesetlocation.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 + } + + void addErrorsAsSuppressedAndClear(final Exception ex) + { + this.errors.forEach(ex::addSuppressed); + this.errors.clear(); + } + } +} diff --git a/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java new file mode 100644 index 0000000..9b43778 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaround.java @@ -0,0 +1,135 @@ +package software.xdev.pmd.model.config.rulesetlocation.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/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/MavenThirdPartyCPLocation.java b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java new file mode 100644 index 0000000..d825fb4 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocation.java @@ -0,0 +1,55 @@ +package software.xdev.pmd.model.config.thirdpartycplocation.maven; + +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; + + +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..288fc04 --- /dev/null +++ b/src/main/java/software/xdev/pmd/model/config/thirdpartycplocation/maven/MavenThirdPartyCPLocationFactory.java @@ -0,0 +1,52 @@ +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.maven.MavenId; +import software.xdev.pmd.maven.resolve.MavenArtifactResolver; +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) + { + 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..8cf140e --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/application/PMDApplicationConfigurable.java @@ -0,0 +1,104 @@ +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(); + + return FormBuilder.createFormBuilder() + .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) + .addComponentFillVertically(new JPanel(), 0) + .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() + { + 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/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 668a80a..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 @@ -1,7 +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; @@ -14,34 +13,25 @@ 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; import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationBuilder; -import software.xdev.pmd.model.config.ConfigurationLocation; +import software.xdev.pmd.config.plugin.PatternContainer; +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.rulesetlocation.RSLocationPanelManager; +import software.xdev.pmd.ui.config.project.components.thirdpartyclasspath.TPCPLocationPanelManager; /** @@ -51,11 +41,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 +49,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 RSLocationPanelManager rsLocationPanelManager; - private final FileMaskPanelContainer exclusionPanelContainer = new FileMaskPanelContainer( + private final FileMaskPanelManager exclusionPanelManager = new FileMaskPanelManager( "Exclusions", "Nothing excluded", "Add exclusion", @@ -86,7 +70,9 @@

Ignores certain files (patterns). """ ); - private final Project project; + private final TPCPLocationPanelManager tpcpLocationPanelManager; + + final Project project; public PMDConfigPanel(@NotNull final Project project) { @@ -94,6 +80,9 @@ public PMDConfigPanel(@NotNull final Project project) this.project = project; + this.rsLocationPanelManager = new RSLocationPanelManager(project, this); + this.tpcpLocationPanelManager = new TPCPLocationPanelManager(project, this); + this.initialise(); } @@ -131,13 +120,26 @@ private JPanel buildConfigPanel() + "It's recommended to only enable this when importing changed configuration."), this.createDefaultGridBagConstraints(0, 2, 2)); - configFilePanel.add( - this.buildRuleFilePanel(), - this.createFullWidthGridBagConstraints(3, 1.0)); + this.addPanel( + configFilePanel, + this.rsLocationPanelManager.panel(), + 3, + 1.0, + 250); - configFilePanel.add( - this.exclusionPanelContainer.getPanel(), - this.createFullWidthGridBagConstraints(4, 0.1)); + this.addPanel( + configFilePanel, + this.exclusionPanelManager.getPanel(), + 4, + 0.1, + 130); + + this.addPanel( + configFilePanel, + this.tpcpLocationPanelManager.panel(), + 5, + 0.1, + 140); return configFilePanel; } @@ -186,47 +188,18 @@ private GridBagConstraints createFullWidthGridBagConstraints(final int gridY, fi 0); } - private JPanel buildRuleFilePanel() + private void addPanel( + final JPanel panel, + final JPanel panelToAdd, + final int gridY, + final double weighty, + final int preferredHeight + ) { - 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); - } + panelToAdd.setPreferredSize(new Dimension(Integer.MAX_VALUE, preferredHeight)); + panel.add( + panelToAdd, + this.createFullWidthGridBagConstraints(gridY, weighty)); } public void showPluginConfiguration(@NotNull final PluginConfiguration pluginConfig) @@ -236,11 +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.locationModel.setLocations(new ArrayList<>(pluginConfig.locations())); - this.locationModel.setActiveLocations(pluginConfig.getActiveLocations()); - this.exclusionPanelContainer.update(pluginConfig.projectRelativeFileExclusions().stream() + 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.tpcpLocationPanelManager.locationModel().setLocations(pluginConfig.thirdPartyCPLocations()); } public PluginConfiguration getPluginConfiguration() @@ -252,88 +226,13 @@ 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.rsLocationPanelManager.locationModel().getLocations())) + .withActiveLocationIds(this.rsLocationPanelManager.locationModel().getActiveLocations().stream() .map(ConfigurationLocation::getId) .collect(Collectors.toCollection(TreeSet::new))) + .withThirdPartyCPLocations(this.tpcpLocationPanelManager.locationModel().getLocations()) .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/PMDConfigurable.java b/src/main/java/software/xdev/pmd/ui/config/project/PMDConfigurable.java index c5bd6fc..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 @@ -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 @@ -34,15 +36,14 @@ public String getDisplayName() return "PMD"; } - @Override - public String getHelpTopic() - { - return null; - } - @Override public JComponent createComponent() { + if(this.configPanel == null) + { + return new JLabel("Project configuration not available"); + } + this.reset(); return this.configPanel; } @@ -55,6 +56,11 @@ private PluginConfiguration getConfigPanelPluginConfig() @Override public boolean isModified() { + if(this.configPanel == null) + { + return false; + } + return !this.pluginConfigurationManager.getCurrent() // Old .isIdentical(this.getConfigPanelPluginConfig()); // New } @@ -62,19 +68,22 @@ 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); - } - - @Override - public void disposeUIResources() - { - // do nothing + if(this.configPanel == null) + { + return; + } + + this.configPanel.showPluginConfiguration(this.pluginConfigurationManager.getCurrent()); } } 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/FileMaskPanelContainer.java b/src/main/java/software/xdev/pmd/ui/config/project/components/exclusion/FileMaskPanelManager.java similarity index 93% rename from src/main/java/software/xdev/pmd/ui/config/project/FileMaskPanelContainer.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/exclusion/FileMaskPanelManager.java index 25f7a1e..74f990e 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/FileMaskPanelContainer.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; +package software.xdev.pmd.ui.config.project.components.exclusion; import java.awt.BorderLayout; import java.awt.Dimension; @@ -19,10 +19,10 @@ import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBList; -import software.xdev.pmd.config.PatternContainer; +import software.xdev.pmd.config.plugin.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/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/LocationPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanel.java similarity index 62% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationPanel.java index d9849b8..80cac5c 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/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; +package software.xdev.pmd.ui.config.project.components.rulesetlocation; -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.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.rulesetlocation.RSLocationPanel.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; @@ -33,13 +37,14 @@ 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; @SuppressWarnings("checkstyle:MagicNumber") -public class LocationPanel extends JPanel +public class RSLocationPanel extends JPanel { enum LocationType { @@ -57,15 +62,11 @@ enum LocationType private final Project project; - public LocationPanel(final Project project) + public RSLocationPanel(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(); } @@ -74,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); @@ -129,20 +131,25 @@ 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() { - if(this.relativeFileCheckbox.isSelected()) - { - return PROJECT_RELATIVE; - } - return LOCAL_FILE; + return this.relativeFileCheckbox.isSelected() + ? PROJECT_RELATIVE + : LOCAL_FILE; } /** @@ -154,52 +161,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 +240,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,38 +256,29 @@ 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(RSLocationPanel.this.getFileLocationPath(LOCAL_FILE)); + } + catch(final Exception ex) + { + fileLocationPath = Optional.empty(); + } + final VirtualFile toSelect = fileLocationPath + .map(LocalFileSystem.getInstance()::findFileByNioFile) + .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()); - } - } - } - - - /** - * Handles radio button selections. - */ - 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/LocationTableModel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationTableModel.java similarity index 80% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationTableModel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/rulesetlocation/RSLocationTableModel.java index 8745e1b..9ef456d 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/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; +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.ConfigurationLocation; +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() { @@ -182,7 +158,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/ui/config/project/CompletePanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/CompletePanel.java similarity index 79% rename from src/main/java/software/xdev/pmd/ui/config/project/CompletePanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/CompletePanel.java index 9b36a59..e63e64f 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/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; +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/ErrorPanel.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/ErrorPanel.java similarity index 77% rename from src/main/java/software/xdev/pmd/ui/config/project/ErrorPanel.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/ErrorPanel.java index 0890342..3cc6af9 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/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; +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/LocationDialog.java b/src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java similarity index 69% rename from src/main/java/software/xdev/pmd/ui/config/project/LocationDialog.java rename to src/main/java/software/xdev/pmd/ui/config/project/components/shared/LocationDialog.java index 66307a4..e942578 100644 --- a/src/main/java/software/xdev/pmd/ui/config/project/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; +package software.xdev.pmd.ui.config.project.components.shared; import java.awt.BorderLayout; import java.awt.Dialog; @@ -17,26 +17,22 @@ 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; import com.intellij.util.ui.JBUI; -import software.xdev.pmd.model.config.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 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), @@ -70,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); @@ -143,7 +148,7 @@ this.commitButton, new GridBagConstraints( return bottomPanel; } - private JPanel panelForCurrentStep() + protected JPanel panelForCurrentStep() { return switch(this.currentStep) { @@ -158,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; @@ -180,29 +185,7 @@ private void moveToStep(final Step newStep) this.centrePanel.repaint(); } - private Step continueWithValidate(final ConfigurationLocation location) - { - this.configurationLocation = location; - - try - { - this.configurationLocation.validate(); - return Step.COMPLETE; - } - catch(final Exception e) - { - this.errorPanel.setError(e); - return Step.ERROR; - } - } - - private Step continueWithoutValidate(final ConfigurationLocation location) - { - this.configurationLocation = location; - return Step.COMPLETE; - } - - void onPrevious() + protected void onPrevious() { this.previousButton.setEnabled(false); @@ -218,34 +201,33 @@ 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: - location = this.locationPanel.getConfigurationLocation(); - if(location == null) - { - this.showError("No location has been entered"); - return; - } - - if(location.getDescription() == null || location.getDescription().isEmpty()) + try { - this.showError("No description has been entered"); - return; + final L location = this.getLocationFromPanelAndValidate(this.locationPanel); + if(location == null) + { + this.commitButton.setEnabled(true); + return; + } + + this.configurationLocation = location; + this.moveToStep(Step.COMPLETE); } - - if(!this.project.isDefault() || location.canBeResolvedInDefaultProject()) + catch(final Exception e) { - this.moveToStep(this.continueWithValidate(location)); - return; + this.errorPanel.setError(e); + this.moveToStep(Step.ERROR); } - this.moveToStep(this.continueWithoutValidate(location)); return; case COMPLETE: @@ -258,7 +240,7 @@ void onNext() } } - private void showError(final String formattedMessage) + protected void showError(final String formattedMessage) { Messages.showErrorDialog( this.getContentPanel(), @@ -267,7 +249,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) @@ -276,9 +258,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..23fa987 --- /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.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.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())) + { + return 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..77d25d0 --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationPanelManager.java @@ -0,0 +1,53 @@ +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 = 150; + + 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(); + 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 new file mode 100644 index 0000000..d71800c --- /dev/null +++ b/src/main/java/software/xdev/pmd/ui/config/project/components/thirdpartyclasspath/TPCPLocationTableModel.java @@ -0,0 +1,67 @@ +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 boolean trySwap(final int index, final int otherIndex) + { + try + { + Collections.swap(this.locations, index, otherIndex); + return true; + } + catch(final IndexOutOfBoundsException ignored) + { + return false; + } + } + + @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/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; + } +} 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 6856ded..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,14 +1,10 @@ package software.xdev.pmd.util.io; -import static java.util.Arrays.asList; - import java.io.File; -import java.util.function.Function; +import java.util.Objects; 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; @@ -16,134 +12,36 @@ 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; - 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) - { - if(path == null || this.project.isDefault()) - { - return path; - } - - final File projectPath = this.projectPath(); - if(projectPath == null) - { - LOG.debug("Couldn't find project path, returning full path: " + path); - return path; - } - - 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; - } } - @Nullable - public String tokenise(@Nullable final String fsPath) + public String makeProjectRelativePathAbsolute(final String path) { - 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); - } - } + Objects.requireNonNull(path, "Invalid path"); - final File projectPath = this.projectPath(); - if(projectPath != null && fsPath.startsWith(this.absolutePathOf.apply(projectPath) + this.separatorChar)) + final VirtualFile projectDir = ProjectUtil.guessProjectDir(this.project); + if(projectDir == null) { - return IDEA_PROJECT_DIR - + this.toUnixPath(fsPath.substring(this.absolutePathOf.apply(projectPath).length())); + throw new IllegalStateException("Unable to guess directory of project " + this.project); } - 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) - { - for(final String projectDirToken : asList(IDEA_PROJECT_DIR, LEGACY_PROJECT_DIR)) - { - 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 + 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); - } - } - } - return null; + return projectDir.toNioPath().toAbsolutePath().toString() + this.separatorChar + path; } - private String toUnixPath(final String systemPath) + public String toUnixPath(final String systemPath) { if(this.separatorChar == '/') { @@ -152,7 +50,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 == '/') { @@ -160,25 +58,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; - } - } } diff --git a/src/main/resources/META-INF/plugin-maven.xml b/src/main/resources/META-INF/plugin-maven.xml index 3933ffe..9ff4a18 100644 --- a/src/main/resources/META-INF/plugin-maven.xml +++ b/src/main/resources/META-INF/plugin-maven.xml @@ -1,6 +1,10 @@ - + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 72a3f86..00d3e0f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -25,22 +25,38 @@ - + + + + + + + + + + + @@ -129,5 +145,9 @@ name="languageVersionResolver" interface="software.xdev.pmd.langversion.LanguageVersionResolver" dynamic="true"/> + diff --git a/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java new file mode 100644 index 0000000..7426e0c --- /dev/null +++ b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/DefaultRuleSetLoaderCreatorTest.java @@ -0,0 +1,17 @@ +package software.xdev.pmd.model.config.rulesetlocation.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/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java new file mode 100644 index 0000000..f95a21a --- /dev/null +++ b/src/test/java/software/xdev/pmd/model/config/rulesetlocation/file/pmd/LoadFromStringRuleSetLoaderWorkaroundTest.java @@ -0,0 +1,15 @@ +package software.xdev.pmd.model.config.rulesetlocation.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); + } +}