From 93cd1b524df6ac0413c2271c4b90c98bd93044b2 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 19 Apr 2015 02:34:12 +0200 Subject: [PATCH] Introduce version 7.5 --- .gitignore | 2 + LICENSE | 7 +- README.md | 84 ++ mainIcon.svg | 118 +++ pluginPortal/FullSizeImage.png | Bin 0 -> 103711 bytes pluginPortal/ThumbnailImage.png | Bin 0 -> 7153 bytes pom.xml | 342 ++++++++ .../easypmd7/DefaultStorageAreaService.java | 72 ++ .../DefaultSystemPropertiesService.java | 86 ++ .../easypmd7/PropertyPluginInfoService.java | 41 + .../easypmd7/StorageAreaService.java | 32 + .../easypmd7/SystemPropertiesService.java | 34 + .../easypmd7/ThrowableExtensions.java | 74 ++ .../easypmd7/docs/package-info.java | 24 + .../easypmd7/ide/DefaultDialogService.java | 90 ++ .../easypmd7/ide/DialogService.java | 42 + .../easypmd7/ide/IdeScanner.java | 195 +++++ .../gianlucacosta/easypmd7/ide/Injector.java | 82 ++ .../ide/editor/AnnotationService.java | 34 + .../ide/editor/DefaultAnnotationService.java | 118 +++ .../ide/editor/GuardedSectionsAnalyzer.java | 105 +++ .../ide/editor/ScanMessageAnnotation.java | 57 ++ .../ide/editor/ScanMessageAnnotationList.java | 41 + .../ide/options/AdditionalClasspathPanel.form | 122 +++ .../ide/options/AdditionalClasspathPanel.java | 237 ++++++ .../easypmd7/ide/options/DefaultOptions.java | 238 ++++++ .../ide/options/DefaultOptionsFactory.java | 110 +++ .../ide/options/DefaultOptionsService.java | 134 +++ .../EasyPmdOptionsPanelController.java | 130 +++ .../easypmd7/ide/options/EasyPmdPanel.form | 625 ++++++++++++++ .../easypmd7/ide/options/EasyPmdPanel.java | 803 ++++++++++++++++++ .../ide/options/InvalidOptionsException.java | 47 + .../easypmd7/ide/options/Options.java | 65 ++ .../easypmd7/ide/options/OptionsFactory.java | 30 + .../easypmd7/ide/options/OptionsService.java | 45 + .../easypmd7/ide/options/OptionsVerifier.java | 30 + .../ide/options/PathFilteringOptions.java | 89 ++ .../ide/options/PathFilteringPanel.form | 81 ++ .../ide/options/PathFilteringPanel.java | 94 ++ .../options/RulePriorityComboBoxModel.java | 39 + .../easypmd7/ide/options/RuleSetsPanel.form | 122 +++ .../easypmd7/ide/options/RuleSetsPanel.java | 231 +++++ .../ide/options/profiles/DefaultProfile.java | 51 ++ .../profiles/DefaultProfileConfiguration.java | 55 ++ .../DefaultProfileConfigurationFactory.java | 58 ++ ...DefaultProfileConfigurationRepository.java | 96 +++ .../options/profiles/DefaultProfileMap.java | 125 +++ .../ide/options/profiles/Profile.java | 34 + .../profiles/ProfileConfiguration.java | 36 + .../profiles/ProfileConfigurationFactory.java | 30 + .../ProfileConfigurationRepository.java | 32 + .../options/profiles/ProfileException.java | 36 + .../ide/options/profiles/ProfileMap.java | 50 ++ .../DefaultRegexTemplateSelectionDialog.java | 65 ++ .../ide/options/regexes/RegexTemplate.java | 42 + .../regexes/RegexTemplateSelectionDialog.java | 30 + .../ide/options/regexes/RegexesPanel.form | 122 +++ .../ide/options/regexes/RegexesPanel.java | 238 ++++++ .../SingleStringParamRegexTemplate.java | 65 ++ .../predefined/AncestorDirectoryRegex.java | 53 ++ .../predefined/CaseInsensitiveRegex.java | 53 ++ .../predefined/FileExtensionRegex.java | 57 ++ .../regexes/predefined/FileNameRegex.java | 53 ++ .../FileNameWithoutExtensionRegex.java | 53 ++ .../regexes/predefined/FullPathRegex.java | 53 ++ .../predefined/ParentDirectoryRegex.java | 53 ++ .../ide/tasklist/ScanMessageTaskList.java | 47 + .../easypmd7/io/StreamUtils.java | 46 + .../CacheBasedLinkedPmdScanningStrategy.java | 58 ++ .../DefaultLanguageVersionParser.java | 70 ++ .../DefaultStandardRuleSetsCatalog.java | 71 ++ .../pmdscanner/LanguageVersionParser.java | 32 + .../pmdscanner/LinkedPmdScanningStrategy.java | 130 +++ .../pmdscanner/NoOpPmdScannerStrategy.java | 36 + .../pmdscanner/PmdBasedClassLoader.java | 47 + .../easypmd7/pmdscanner/PmdScanner.java | 57 ++ .../pmdscanner/PmdScannerStrategy.java | 32 + .../easypmd7/pmdscanner/RuleSetWrapper.java | 45 + .../easypmd7/pmdscanner/ScanError.java | 74 ++ .../easypmd7/pmdscanner/ScanMessage.java | 42 + .../easypmd7/pmdscanner/ScanMessageList.java | 45 + .../easypmd7/pmdscanner/ScanViolation.java | 153 ++++ .../pmdscanner/StandardRuleSetsCatalog.java | 34 + .../AbstractScanMessagesCache.java | 95 +++ .../InMemoryScanMessagesCache.java | 51 ++ .../messagescache/ScanMessagesCache.java | 38 + .../ScanMessagesCacheFacade.java | 90 ++ .../messagescache/ScanMessagesCacheItem.java | 53 ++ .../StorageAreaBasedScanMessagesCache.java | 143 ++++ src/main/nbm/manifest.mf | 4 + .../gianlucacosta/easypmd7/Bundle.properties | 26 + .../easypmd7/Plugin.properties.xml | 30 + .../gianlucacosta/easypmd7/docs/about.html | 60 ++ .../easypmd7/docs/easypmd-hs.xml | 51 ++ .../easypmd7/docs/easypmd-idx.xml | 30 + .../easypmd7/docs/easypmd-map.xml | 30 + .../easypmd7/docs/easypmd-toc.xml | 32 + .../gianlucacosta/easypmd7/docs/easypmd.css | 36 + .../gianlucacosta/easypmd7/docs/options.html | 347 ++++++++ .../easypmd7/ide/Bundle.properties | 23 + .../ide/annotations/highDescriptor.xml | 34 + .../ide/annotations/lowDescriptor.xml | 34 + .../ide/annotations/mediumDescriptor.xml | 34 + .../ide/annotations/mediumHighDescriptor.xml | 34 + .../ide/annotations/mediumLowDescriptor.xml | 34 + .../ide/annotations/scanErrorDescriptor.xml | 34 + .../gianlucacosta/easypmd7/ide/icons/high.png | Bin 0 -> 373 bytes .../gianlucacosta/easypmd7/ide/icons/low.png | Bin 0 -> 385 bytes .../easypmd7/ide/icons/medium.png | Bin 0 -> 353 bytes .../easypmd7/ide/icons/mediumHigh.png | Bin 0 -> 295 bytes .../easypmd7/ide/icons/mediumLow.png | Bin 0 -> 363 bytes .../easypmd7/ide/icons/scanError.png | Bin 0 -> 418 bytes .../easypmd7/ide/options/Bundle.properties | 73 ++ .../ide/options/regexes/Bundle.properties | 27 + .../easypmd7/ide/tasklist/Bundle.properties | 45 + .../info/gianlucacosta/easypmd7/layer.xml | 129 +++ .../gianlucacosta/easypmd7/mainIcon128.png | Bin 0 -> 7153 bytes .../gianlucacosta/easypmd7/mainIcon32.png | Bin 0 -> 1591 bytes 118 files changed, 9124 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 mainIcon.svg create mode 100644 pluginPortal/FullSizeImage.png create mode 100644 pluginPortal/ThumbnailImage.png create mode 100644 pom.xml create mode 100644 src/main/java/info/gianlucacosta/easypmd7/DefaultStorageAreaService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/DefaultSystemPropertiesService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/PropertyPluginInfoService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/StorageAreaService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/SystemPropertiesService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ThrowableExtensions.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/docs/package-info.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/DefaultDialogService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/DialogService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/IdeScanner.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/Injector.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/editor/AnnotationService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/editor/DefaultAnnotationService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/editor/GuardedSectionsAnalyzer.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotation.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotationList.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.form create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptions.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsFactory.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdOptionsPanelController.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.form create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/InvalidOptionsException.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/Options.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsFactory.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsService.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsVerifier.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringOptions.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.form create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/RulePriorityComboBoxModel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.form create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfile.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfiguration.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationFactory.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationRepository.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileMap.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/Profile.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfiguration.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationFactory.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationRepository.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileException.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileMap.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/DefaultRegexTemplateSelectionDialog.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplate.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplateSelectionDialog.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.form create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/SingleStringParamRegexTemplate.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/AncestorDirectoryRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/CaseInsensitiveRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileExtensionRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameWithoutExtensionRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FullPathRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/ParentDirectoryRegex.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/ide/tasklist/ScanMessageTaskList.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/io/StreamUtils.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/CacheBasedLinkedPmdScanningStrategy.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultLanguageVersionParser.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultStandardRuleSetsCatalog.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LanguageVersionParser.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LinkedPmdScanningStrategy.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/NoOpPmdScannerStrategy.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdBasedClassLoader.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScanner.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScannerStrategy.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/RuleSetWrapper.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanError.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessage.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessageList.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanViolation.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/StandardRuleSetsCatalog.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/AbstractScanMessagesCache.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/InMemoryScanMessagesCache.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCache.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheFacade.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheItem.java create mode 100644 src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/StorageAreaBasedScanMessagesCache.java create mode 100644 src/main/nbm/manifest.mf create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/Bundle.properties create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/Plugin.properties.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/about.html create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-hs.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-idx.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-map.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-toc.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd.css create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/docs/options.html create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/Bundle.properties create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/highDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/lowDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumHighDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumLowDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/scanErrorDescriptor.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/high.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/low.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/medium.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumHigh.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumLow.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/icons/scanError.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/options/Bundle.properties create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/options/regexes/Bundle.properties create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/ide/tasklist/Bundle.properties create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/layer.xml create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/mainIcon128.png create mode 100644 src/main/resources/info/gianlucacosta/easypmd7/mainIcon32.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2436af1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/** +**/*.html_* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 733c072..94a9ed0 100644 --- a/LICENSE +++ b/LICENSE @@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} + + Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - {project} Copyright (C) {year} {fullname} + Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. @@ -672,4 +672,3 @@ may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . - diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6f20d0 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# EasyPmd + +*Seamlessly integrates PMD into your NetBeans IDE* + + +## Introduction + +*Elegance always matters, especially when +creating software.* + +However, a universal definition of such an ambitious goal seems to be fairly difficult, if not impossible, as different programmers adopt different styles. + +PMD is a Java library/tool allowing you to scan your code and detecting *violations* of the *rules* that you requested to enforce: this introduces a great deal of flexibility, in particular if you consider that you can both use a wide range of predefined rules *and* write your own rules: for example, the predefined ruleset *rulesets/unusedcode.xml* will make PMD scan your source files for unused private fields, unused private methods, unused local variables and so on. Of course, you can instruct PMD to use *multiple rulesets simultaneously*. + +PMD can be run both as a library and as a standalone program, and several excellent IDE plugins are now available, some of which target NetBeans: EasyPmd is designed as an open source plugin for NetBeans which seamlessly integrates the PMD scanning engine into NetBeans, by making PMD violation reports automatically appear both in the *Action items* window and in the editor: you just have to specify the scanning scope (current file, main project, all projects) in the *Action items* window. + +The current major version provides a simplified, more elegant architecture, based on Helios, and *profiles*, to let you easily switch between multiple configurations. + +The overall build process is based on Maven 3, for elegance and robustness. + + +## Features + +* Includes PMD 5, compatible with Java 8. + +* Fully-refactored, much better and faster architecture, relying on Helios and Maven 3. + +* Automatically runs PMD on the files of your current task scope (selected in the *Action items* window). + +* Reports every PMD violation both in the *Action Items* window and in the editor's side bar. + +* Option field for setting PMD's *auxiliary classpath* + +* **Supports profiles:** you can easily change the plugin's options simply by changing the active profile. + +* **Glyphs of different colors for different priorities:** ranging from full green to full red - according to the priority of the rule that is is bound to each violation. + +* **Priority filtering:** in the configuration dialog, one can choose the minimum priority level that PMD will consider when applying rules. + +* **Optional priority label in task descriptions:** each violation in the *Action items* window shows its priority by default - one can therefore sort violations just by clicking the *Description* header. + +* Includes a copy of PMD, for a safer and much faster execution. + +* **Robustness:** most execution errors, should they occur, are caught and reported in the tasks list, without crashing the plugin. + +* **Integrated cache:** to ensure maximum speed and avoid repeated scans, EasyPmd features a cache which is also persisted to disk, so it is available even after you restart NetBeans. + +* You can extensively customize EasyPmd and the underlying PMD engine via a user-friendly options dialog. + +* **Custom path filtering** (including and excluding paths), based on *regular expressions*. + +* Predefined, customizable regular expressions, to simplify path filtering. + +* Online help, integrated into the NetBeans help system. + + + +## Requirements + +EasyPmd 7 requires NetBeans 8+ and Java 8+. + + + +## Installation + +EasyPmd can be easily installed from within NetBeans, as explained in the tutorials below. + +Alternatively, you can download the **.nbm** files from GitHub or from the NetBeans Plugin Portal. + + + + +## Tutorials on YouTube + +* [Getting started - English version](https://www.youtube.com/watch?v=BsMx6PNn0aI) + +* [Getting started - Version française](https://www.youtube.com/watch?v=3k4Fk43u0QE) + + + +## Further references + +* [PMD](http://pmd.sourceforge.net/) +* [Facebook page](https://www.facebook.com/easypmd) diff --git a/mainIcon.svg b/mainIcon.svg new file mode 100644 index 0000000..b0402f4 --- /dev/null +++ b/mainIcon.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/pluginPortal/FullSizeImage.png b/pluginPortal/FullSizeImage.png new file mode 100644 index 0000000000000000000000000000000000000000..68296123db8026d984bd95396eb3cb26c0dca3c0 GIT binary patch literal 103711 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 literal 0 HcmV?d00001 diff --git a/pluginPortal/ThumbnailImage.png b/pluginPortal/ThumbnailImage.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5e8f554cdc121b23750ecea520447095f5b254 GIT binary patch literal 7153 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 TO>^$UhXGJgREL$nFc0}ZF66Lg literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..738f66b --- /dev/null +++ b/pom.xml @@ -0,0 +1,342 @@ + + 4.0.0 + + info.gianlucacosta.easypmd + easypmd + 7.5 + nbm + + + EasyPmd + Seamlessly integrates PMD into your NetBeans IDE + https://github.com/giancosta86/EasyPmd + 2009 + + + + + Gianluca Costa + gianluca@gianlucacosta.info + http://gianlucacosta.info/ + + + + + + + GPLv3 + http://www.gnu.org/licenses/gpl.html + repo + + + + + + true + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + https://www.facebook.com/easypmd + + + + + + hephaestus + Hephaestus + + https://dl.bintray.com/giancosta86/Hephaestus + + + false + + + + + + netbeans-repository + NetBeans repository + http://bits.netbeans.org/maven2/ + + false + + + + + + + scm:git:git://github.com/giancosta86/EasyPmd.git + scm:git:git@github.com:giancosta86/EasyPmd.git + https://github.com/giancosta86/EasyPmd + + + + + + info.gianlucacosta.helios + helios-core + 1.7 + + + + net.sourceforge.pmd + pmd-core + 5.3.0 + + + + xerces + xercesImpl + + + + net.sourceforge.saxon + saxon + + + + + + + net.sourceforge.pmd + pmd-java + 5.3.0 + + + + xerces + xercesImpl + + + + net.sourceforge.saxon + saxon + + + + + + + org.netbeans.api + org-netbeans-api-annotations-common + RELEASE80 + + + + org.netbeans.api + org-openide-dialogs + RELEASE80 + + + + org.netbeans.api + org-netbeans-spi-tasklist + RELEASE80 + + + + org.netbeans.api + org-openide-text + RELEASE80 + + + + org.netbeans.api + org-openide-loaders + RELEASE80 + + + + org.netbeans.api + org-netbeans-modules-editor-guards + RELEASE80 + + + + org.netbeans.api + org-netbeans-modules-options-api + RELEASE80 + + + + org.netbeans.api + org-openide-awt + RELEASE80 + + + + org.netbeans.api + org-openide-util + RELEASE80 + + + + org.netbeans.api + org-openide-util-lookup + RELEASE80 + + + + org.netbeans.api + org-openide-filesystems + RELEASE80 + + + + org.netbeans.api + org-netbeans-modules-javahelp + RELEASE80 + + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0-alpha-2 + + + initialize + + read-project-properties + + + + ${jks_signing_params_file} + + + true + + + + + + + + org.codehaus.mojo + license-maven-plugin + 1.8 + + + + add-license-information + process-sources + + + update-project-license + update-file-header + + + + Gianluca Costa + gpl_v3 + + true + true + true + + false + + ${project.basedir}/LICENSE + + + ==========================================================================%%# + + + ===========================================================================%% + + + ==========================================================================%## + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + + true + true + + + + + + org.codehaus.mojo + nbm-maven-plugin + 3.14 + true + + + ${jar.signing.keystore.path} + ${jar.signing.keystore.password} + ${jar.signing.key.alias} + true + GPLv3 + LICENSE + + + + + + + + src/main/resources + true + + **/*.properties.xml + + + + + src/main/resources + false + + **/*.properties.xml + + + + + src/test/resources + true + + **/*.properties.xml + + + + + src/test/resources + false + + **/*.properties.xml + + + + + diff --git a/src/main/java/info/gianlucacosta/easypmd7/DefaultStorageAreaService.java b/src/main/java/info/gianlucacosta/easypmd7/DefaultStorageAreaService.java new file mode 100644 index 0000000..8ef80ac --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/DefaultStorageAreaService.java @@ -0,0 +1,72 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.helios.io.Directory; +import info.gianlucacosta.helios.io.storagearea.DirectoryStorageArea; +import info.gianlucacosta.helios.io.storagearea.StorageArea; +import org.openide.util.lookup.ServiceProvider; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default implementation of StorageAreaService + */ +@ServiceProvider(service = StorageAreaService.class) +public class DefaultStorageAreaService implements StorageAreaService { + private static final Logger logger = Logger.getLogger(DefaultStorageAreaService.class.getName()); + + private final SystemPropertiesService systemPropertiesService; + private final PropertyPluginInfoService pluginInfoService; + private final String storageDirName; + private final StorageArea storageArea; + + + public DefaultStorageAreaService() { + systemPropertiesService = Injector.lookup(SystemPropertiesService.class); + pluginInfoService = Injector.lookup(PropertyPluginInfoService.class); + + String majorVersion = pluginInfoService.getVersion().split("\\.")[0]; + storageDirName = String.format(".EasyPmd%s", majorVersion); + + StorageArea tempStorageArea; + + Directory userHomeDir = systemPropertiesService.getUserHomeDir(); + if (userHomeDir != null) { + Directory storageDir = new Directory(userHomeDir, storageDirName); + + tempStorageArea = new DirectoryStorageArea(storageDir); + } else { + tempStorageArea = null; + logger.log(Level.SEVERE, "Cannot determine the home directory: the storage area will NOT be available"); + } + + storageArea = tempStorageArea; + } + + @Override + public StorageArea getStorageArea() { + return storageArea; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/DefaultSystemPropertiesService.java b/src/main/java/info/gianlucacosta/easypmd7/DefaultSystemPropertiesService.java new file mode 100644 index 0000000..df27eeb --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/DefaultSystemPropertiesService.java @@ -0,0 +1,86 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import info.gianlucacosta.helios.io.Directory; +import org.openide.util.lookup.ServiceProvider; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Default implementation of SystemPropertiesService + */ +@ServiceProvider(service = SystemPropertiesService.class) +public class DefaultSystemPropertiesService implements SystemPropertiesService { + + public String javaVersion; + public Directory userHomeDir; + + private static String findJavaVersion() { + Pattern versionPattern = Pattern.compile("(\\d\\.\\d).*"); + + String javaVersionProperty = System.getProperty("java.version"); + Matcher versionMatcher = versionPattern.matcher(javaVersionProperty); + + if (versionMatcher.matches()) { + return versionMatcher.group(1); + } else { + return null; + } + } + + private static Directory findUserHomeDir() { + String userHomeString = System.getProperty("user.home"); + + if (userHomeString == null) { + return null; + } + + File result = new File(userHomeString); + + if (result.exists() && result.isDirectory()) { + return new Directory(result); + } + + return null; + } + + @Override + public String getJavaVersion() { + if (javaVersion == null) { + javaVersion = findJavaVersion(); + } + + return javaVersion; + } + + @Override + public Directory getUserHomeDir() { + if (userHomeDir == null) { + userHomeDir = findUserHomeDir(); + } + + return userHomeDir; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/PropertyPluginInfoService.java b/src/main/java/info/gianlucacosta/easypmd7/PropertyPluginInfoService.java new file mode 100644 index 0000000..15da59e --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/PropertyPluginInfoService.java @@ -0,0 +1,41 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import info.gianlucacosta.helios.product.ProductInfoService; +import info.gianlucacosta.helios.product.PropertyProductInfoService; +import info.gianlucacosta.helios.properties.XmlProperties; +import org.openide.util.lookup.ServiceProvider; + +import java.io.IOException; + +/** + * Reads the plugin information from a property file + */ +@ServiceProvider(service = ProductInfoService.class) +public class PropertyPluginInfoService extends PropertyProductInfoService { + + public PropertyPluginInfoService() throws IOException { + super(new XmlProperties("/info/gianlucacosta/easypmd7/Plugin.properties.xml")); + } + +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/StorageAreaService.java b/src/main/java/info/gianlucacosta/easypmd7/StorageAreaService.java new file mode 100644 index 0000000..b0134ea --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/StorageAreaService.java @@ -0,0 +1,32 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import info.gianlucacosta.helios.io.storagearea.StorageArea; + +/** + * Provides a storage area + */ +public interface StorageAreaService { + + StorageArea getStorageArea(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/SystemPropertiesService.java b/src/main/java/info/gianlucacosta/easypmd7/SystemPropertiesService.java new file mode 100644 index 0000000..3cd231e --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/SystemPropertiesService.java @@ -0,0 +1,34 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import info.gianlucacosta.helios.io.Directory; + +/** + * Provides system properties + */ +public interface SystemPropertiesService { + + String getJavaVersion(); + + Directory getUserHomeDir(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ThrowableExtensions.java b/src/main/java/info/gianlucacosta/easypmd7/ThrowableExtensions.java new file mode 100644 index 0000000..dc9df2d --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ThrowableExtensions.java @@ -0,0 +1,74 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +/** + * Throwable-related utilities + */ +public class ThrowableExtensions { + + private ThrowableExtensions() { + //Just do nothing + } + + /** + * Converts the stack trace of a Throwable to a string + * + * @param throwable The subject throwable + * @return the string representation of the stack trace + */ + public static String getStackTraceString(Throwable throwable) { + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + + throwable.printStackTrace(printWriter); + + return result.toString(); + } + + /** + * Returns the throwable message, or the message of its cause, recursively, + * or a simple message if no message was found in the chain + * + * @param throwable The subject throwable + * @return a non-empty string + */ + public static String getNonEmptyMessage(Throwable throwable) { + String message = throwable.getMessage(); + + if (message == null || message.isEmpty()) { + Throwable cause = throwable.getCause(); + + if (cause != null) { + return getNonEmptyMessage(throwable); + } else { + return "(no message)"; + } + } + + return message; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/docs/package-info.java b/src/main/java/info/gianlucacosta/easypmd7/docs/package-info.java new file mode 100644 index 0000000..6276efd --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/docs/package-info.java @@ -0,0 +1,24 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +@HelpSetRegistration(helpSet = "easypmd-hs.xml", position = 3942) package info.gianlucacosta.easypmd7.docs; + +import org.netbeans.api.javahelp.HelpSetRegistration; diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/DefaultDialogService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/DefaultDialogService.java new file mode 100644 index 0000000..1c44e53 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/DefaultDialogService.java @@ -0,0 +1,90 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide; + +import info.gianlucacosta.helios.application.io.CommonQuestionOutcome; +import info.gianlucacosta.helios.product.ProductInfoService; +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; +import org.openide.util.lookup.ServiceProvider; + +import javax.swing.*; + +/** + * Default implementation of DialogService + */ +@ServiceProvider(service = DialogService.class) +public class DefaultDialogService implements DialogService { + + private final ProductInfoService pluginInfoService; + + public DefaultDialogService() { + pluginInfoService = Injector.lookup(ProductInfoService.class); + } + + private void showMessageBox(String message, int kind) { + NotifyDescriptor messageDescriptor = new NotifyDescriptor.Message(message, kind); + messageDescriptor.setTitle(pluginInfoService.getName()); + DialogDisplayer.getDefault().notifyLater(messageDescriptor); + } + + @Override + public void showInfo(String message) { + showMessageBox(message, NotifyDescriptor.INFORMATION_MESSAGE); + } + + @Override + public void showWarning(String message) { + showMessageBox(message, NotifyDescriptor.WARNING_MESSAGE); + } + + @Override + public void showError(String message) { + showMessageBox(message, NotifyDescriptor.ERROR_MESSAGE); + } + + @Override + public String askForString(String message) { + return JOptionPane.showInputDialog(null, message, pluginInfoService.getName(), JOptionPane.INFORMATION_MESSAGE | JOptionPane.OK_CANCEL_OPTION); + } + + @Override + public String askForString(String message, String defaultValue) { + return JOptionPane.showInputDialog(null, message, defaultValue); + } + + @Override + public CommonQuestionOutcome askYesNoQuestion(String message) { + int dialogResult = JOptionPane.showConfirmDialog(null, message, pluginInfoService.getName(), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + + switch (dialogResult) { + case JOptionPane.YES_OPTION: + return CommonQuestionOutcome.YES; + + case JOptionPane.NO_OPTION: + return CommonQuestionOutcome.NO; + + default: + throw new IllegalStateException(); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/DialogService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/DialogService.java new file mode 100644 index 0000000..ea73c34 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/DialogService.java @@ -0,0 +1,42 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide; + +import info.gianlucacosta.helios.application.io.CommonQuestionOutcome; + +/** + * Can show different types of dialogs + */ +public interface DialogService { + + void showInfo(String message); + + void showWarning(String message); + + void showError(String message); + + String askForString(String message); + + String askForString(String message, String defaultValue); + + CommonQuestionOutcome askYesNoQuestion(String message); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/IdeScanner.java b/src/main/java/info/gianlucacosta/easypmd7/ide/IdeScanner.java new file mode 100644 index 0000000..c553317 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/IdeScanner.java @@ -0,0 +1,195 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide; + +import info.gianlucacosta.easypmd7.ide.editor.AnnotationService; +import info.gianlucacosta.easypmd7.ide.editor.GuardedSectionsAnalyzer; +import info.gianlucacosta.easypmd7.ide.editor.ScanMessageAnnotationList; +import info.gianlucacosta.easypmd7.ide.options.Options; +import info.gianlucacosta.easypmd7.ide.options.OptionsService; +import info.gianlucacosta.easypmd7.ide.tasklist.ScanMessageTaskList; +import info.gianlucacosta.easypmd7.pmdscanner.PmdScanner; +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; +import org.netbeans.spi.tasklist.FileTaskScanner; +import org.netbeans.spi.tasklist.Task; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataObject; +import org.openide.loaders.DataObjectNotFoundException; +import org.openide.util.NbBundle; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The link between the IDE and the plugin's scan system + */ +public class IdeScanner extends FileTaskScanner { + + private static final Logger logger = Logger.getLogger(IdeScanner.class.getName()); + private static final String OPTIONS_PATH = "Advanced/info.gianlucacosta.easypmd7"; + private final AnnotationService annotationService; + private final DialogService dialogService; + private final OptionsService optionsService; + private Callback callback; + private PmdScanner pmdScanner; + private Options options; + private final Lock readOptionsLock; + private final Lock writeOptionsLock; + + public IdeScanner(String displayName, String description, String optionsPath) { + super(displayName, description, optionsPath); + + dialogService = Injector.lookup(DialogService.class); + annotationService = Injector.lookup(AnnotationService.class); + optionsService = Injector.lookup(OptionsService.class); + + ReadWriteLock optionsLock = new ReentrantReadWriteLock(); + readOptionsLock = optionsLock.readLock(); + writeOptionsLock = optionsLock.writeLock(); + + options = optionsService.getOptions(); + + try { + pmdScanner = new PmdScanner(options); + } catch (RuntimeException ex) { + showScannerConfigurationException(ex); + } + + optionsService.addOptionsChangedListener(() -> { + logger.log(Level.INFO, "The options have changed: reinitializing the IDE scanner"); + + writeOptionsLock.lock(); + + try { + annotationService.detachAllAnnotations(); + + options = optionsService.getOptions(); + + try { + pmdScanner = new PmdScanner(options); + } catch (RuntimeException ex) { + pmdScanner = null; + showScannerConfigurationException(ex); + } + + if (callback != null) { + callback.refreshAll(); + } + } finally { + writeOptionsLock.unlock(); + } + }); + } + + private void showScannerConfigurationException(Exception ex) { + logger.log(Level.WARNING, "Configuration exception for EasyPmd:\n%s", ex); + + dialogService.showWarning(String.format("Could not run EasyPmd because of configuration errors:\n\t%s (%s)", ex.getMessage(), ex.getClass().getSimpleName())); + } + + @Override + public List scan(FileObject fileObject) { + if (pmdScanner == null) { + return new ScanMessageTaskList(); + } + + readOptionsLock.lock(); + try { + File file = FileUtil.toFile(fileObject); + + if (file == null || !file.isFile()) { + return new ScanMessageTaskList(); + } + + annotationService.detachAnnotationsFrom(fileObject); + + String filePath = file.getPath(); + + if (!options.getPathFilteringOptions().isPathValid(filePath)) { + return new ScanMessageTaskList(); + } + + DataObject dataObject; + try { + dataObject = DataObject.find(fileObject); + } catch (DataObjectNotFoundException ex) { + throw new RuntimeException(ex); + } + + ScanMessageList scanMessages = pmdScanner.scanFile(file); + + if (!options.isShowAllMessagesInGuardedSections()) { + GuardedSectionsAnalyzer guardedSectionsAnalyzer = new GuardedSectionsAnalyzer(dataObject); + + Set guardedPmdLineNumbers = guardedSectionsAnalyzer.getGuardedLineNumbers(); + + if (!guardedPmdLineNumbers.isEmpty()) { + scanMessages = scanMessages.filterOutGuardedSections(guardedPmdLineNumbers); + } + } + + ScanMessageTaskList tasks = new ScanMessageTaskList(fileObject, scanMessages); + + if (options.isShowAnnotationsInEditor()) { + ScanMessageAnnotationList annotations = new ScanMessageAnnotationList(scanMessages); + annotationService.attachAnnotationsTo(dataObject, annotations); + } + + return tasks; + } finally { + readOptionsLock.unlock(); + } + + } + + @Override + public void attach(Callback callback) { + + logger.log(Level.INFO, String.format("Attaching callback: %s", callback)); + + this.callback = callback; + + if (callback == null) { + annotationService.detachAllAnnotations(); + } else { + callback.refreshAll(); + } + } + + public static IdeScanner create() throws ParserConfigurationException { + ResourceBundle bundle = NbBundle.getBundle(IdeScanner.class); + + return new IdeScanner( + bundle.getString("Filter_DisplayName"), + bundle.getString("Filter_Description"), + OPTIONS_PATH); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/Injector.java b/src/main/java/info/gianlucacosta/easypmd7/ide/Injector.java new file mode 100644 index 0000000..9b7d080 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/Injector.java @@ -0,0 +1,82 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide; + +import org.openide.util.Lookup; +import org.openide.util.lookup.ServiceProvider; + +import java.util.Collection; + +/** + * Provides a simplified and checked lookup system + */ +public class Injector { + + @ServiceProvider(service = TestService.class) + public static class TestService { + } + + private static final Lookup lookup = Lookup.getDefault(); + + private static Lookup getLookup() { + return lookup; + } + + public static T lookup(Class serviceClass) { + T result = getLookup().lookup(serviceClass); + + validateLookupResult(serviceClass, result); + + return result; + } + + public static Collection lookupAll(Class serviceClass) { + Collection result = getLookup().lookupAll(serviceClass); + + validateLookupResult(serviceClass, result); + + return result; + } + + private static void validateLookupResult(Class serviceClass, Object lookupResult) { + if (lookupResult == null) { + if (shouldThrowLookupExceptions(serviceClass)) { + throw new IllegalArgumentException(String.format("Service '%s' not registered!", serviceClass.getSimpleName())); + } + } + } + + //This code prevents exceptions in the visual form editors within NetBeans + private static boolean shouldThrowLookupExceptions(Class serviceClass) { + if (serviceClass != TestService.class) { + TestService testService = lookup.lookup(TestService.class); + if (testService == null) { + return false; + } + } + + return true; + } + + private Injector() { + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/editor/AnnotationService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/AnnotationService.java new file mode 100644 index 0000000..af3e372 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/AnnotationService.java @@ -0,0 +1,34 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.editor; + +import org.openide.filesystems.FileObject; +import org.openide.loaders.DataObject; + +public interface AnnotationService { + + void attachAnnotationsTo(DataObject dataObject, ScanMessageAnnotationList annotations); + + void detachAnnotationsFrom(FileObject fileObject); + + void detachAllAnnotations(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/editor/DefaultAnnotationService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/DefaultAnnotationService.java new file mode 100644 index 0000000..68ce19a --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/DefaultAnnotationService.java @@ -0,0 +1,118 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.editor; + +import org.openide.cookies.LineCookie; +import org.openide.filesystems.FileChangeAdapter; +import org.openide.filesystems.FileChangeListener; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileObject; +import org.openide.loaders.DataObject; +import org.openide.text.Annotation; +import org.openide.text.Line; +import org.openide.util.lookup.ServiceProvider; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation of AnnotationService + */ +@ServiceProvider(service = AnnotationService.class) +public class DefaultAnnotationService implements AnnotationService { + + private final Map attachedAnnotations = new HashMap<>(); + private final Map registeredFileChangeListeners = new HashMap<>(); + + @Override + public void attachAnnotationsTo(DataObject dataObject, ScanMessageAnnotationList annotations) { + final FileObject fileObject = dataObject.getPrimaryFile(); + + if (attachedAnnotations.containsKey(fileObject)) { + throw new IllegalArgumentException("EasyPmd annotations have already been attached to this file.\nYou must detach them before attaching new ones."); + } + + LineCookie lineCookie = dataObject.getLookup().lookup(LineCookie.class); + Line.Set lineSet = lineCookie.getLineSet(); + + for (ScanMessageAnnotation annotation : annotations) { + annotation.attach(lineSet); + } + + attachedAnnotations.put(fileObject, annotations); + + registerFileEventHandlers(fileObject); + } + + private void registerFileEventHandlers(final FileObject fileObject) { + FileChangeListener fileChangeListener = new FileChangeAdapter() { + @Override + public void fileChanged(FileEvent ev) { //This is triggered only when the user SAVES the file + detachAnnotationsFrom(fileObject); + } + + @Override + public void fileDeleted(FileEvent ev) { + detachAnnotationsFrom(fileObject); + } + }; + + fileObject.addFileChangeListener(fileChangeListener); + registeredFileChangeListeners.put(fileObject, fileChangeListener); + } + + @Override + public void detachAnnotationsFrom(FileObject fileObject) { + ScanMessageAnnotationList fileAnnotations = attachedAnnotations.get(fileObject); + + if (fileAnnotations == null) { + return; + } + + for (Annotation annotation : fileAnnotations) { + annotation.detach(); + } + + attachedAnnotations.remove(fileObject); + + unregisterFileEventHandlers(fileObject); + } + + private void unregisterFileEventHandlers(FileObject fileObject) { + FileChangeListener fileChangeListener = registeredFileChangeListeners.get(fileObject); + + fileObject.removeFileChangeListener(fileChangeListener); + + registeredFileChangeListeners.remove(fileObject); + } + + @Override + public void detachAllAnnotations() { + Collection fileObjects = new ArrayList<>(attachedAnnotations.keySet()); + + for (FileObject fileObject : fileObjects) { + detachAnnotationsFrom(fileObject); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/editor/GuardedSectionsAnalyzer.java b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/GuardedSectionsAnalyzer.java new file mode 100644 index 0000000..b842414 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/GuardedSectionsAnalyzer.java @@ -0,0 +1,105 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.editor; + +import org.netbeans.api.editor.guards.GuardedSection; +import org.netbeans.api.editor.guards.GuardedSectionManager; +import org.openide.cookies.EditorCookie; +import org.openide.loaders.DataObject; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Position; +import javax.swing.text.StyledDocument; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Analyzes the guarded sections in a document + */ +public class GuardedSectionsAnalyzer { + + private final Set guardedLineNumbers = new HashSet<>(); + + public GuardedSectionsAnalyzer(DataObject dataObject) { + try { + EditorCookie documentCookie = dataObject.getLookup().lookup(EditorCookie.class); + StyledDocument document = documentCookie.openDocument(); + initialize(document); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public GuardedSectionsAnalyzer(StyledDocument document) { + initialize(document); + } + + private void initialize(StyledDocument document) { + try { + String fileContents = document.getText(0, document.getLength()); + if (fileContents.isEmpty()) { + return; + } + + GuardedSectionManager guardedSectionManager = GuardedSectionManager.getInstance(document); + if (guardedSectionManager == null) { + return; + } + + Iterable guardedSections = guardedSectionManager.getGuardedSections(); + if (!guardedSections.iterator().hasNext()) { + return; + } + + int lastLineSeparatorIndex = -1; + + for (int lineNumber = 1; ; lineNumber++) { + int lineStartIndex = lastLineSeparatorIndex + 1; + Position lineStartPosition = document.createPosition(lineStartIndex); + + for (GuardedSection guardedSection : guardedSections) { + if (guardedSection.contains(lineStartPosition, true)) { + guardedLineNumbers.add(lineNumber); + break; + } + } + + lastLineSeparatorIndex = fileContents.indexOf("\n", lastLineSeparatorIndex + 1); + if (lastLineSeparatorIndex == -1) { + break; + } + } + + } catch (BadLocationException ex) { + throw new RuntimeException(ex); + } + } + + /** + * @return the list of guarded line numbers. Line numbers are 1-based. + */ + public Set getGuardedLineNumbers() { + return Collections.unmodifiableSet(guardedLineNumbers); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotation.java b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotation.java new file mode 100644 index 0000000..45df072 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotation.java @@ -0,0 +1,57 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.editor; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessage; +import org.openide.text.Annotation; +import org.openide.text.Line; + +/** + * An editor annotation related to a scan message + */ +public class ScanMessageAnnotation extends Annotation { + + private final ScanMessage message; + + ScanMessageAnnotation(ScanMessage message) { + this.message = message; + } + + public void attach(Line.Set lineSet) { + Line line = lineSet.getOriginal(getLineNumber() - 1); + attach(line); + } + + @Override + public String getAnnotationType() { + return message.getAnnotationType(); + } + + @Override + public String getShortDescription() { + return message.getAnnotationText(); + } + + public int getLineNumber() { + return message.getLineNumber(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotationList.java b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotationList.java new file mode 100644 index 0000000..ccd56b5 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/editor/ScanMessageAnnotationList.java @@ -0,0 +1,41 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.editor; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessage; +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; + +import java.util.ArrayList; + +/** + * A list of scan message annotations + */ +public class ScanMessageAnnotationList extends ArrayList { + + public ScanMessageAnnotationList(ScanMessageList scanMessages) { + for (ScanMessage scanMessage : scanMessages) { + ScanMessageAnnotation currentAnnotation = new ScanMessageAnnotation(scanMessage); + + add(currentAnnotation); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.form b/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.form new file mode 100644 index 0000000..4352168 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.form @@ -0,0 +1,122 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.java new file mode 100644 index 0000000..028a4a6 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/AdditionalClasspathPanel.java @@ -0,0 +1,237 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.ide.DialogService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.helios.swing.dialogs.JarFileChooser; +import info.gianlucacosta.helios.swing.jlist.AdvancedSelectionListModel; +import org.openide.util.Utilities; + +import javax.swing.*; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; + +/** + * Panel for managing the "Additional classpath" option + */ +public class AdditionalClasspathPanel extends JPanel { + + private static final JarFileChooser pathChooser = new JarFileChooser("Select path..."); + private final AdvancedSelectionListModel additionalClasspathModel = new AdvancedSelectionListModel<>(); + private final DialogService dialogService; + + public AdditionalClasspathPanel() { + initComponents(); + + dialogService = Injector.lookup(DialogService.class); + + additionalClasspathList.setModel(additionalClasspathModel); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + additionalClasspathScrollPane = new javax.swing.JScrollPane(); + additionalClasspathList = new info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList(); + buttonsPanel = new javax.swing.JPanel(); + addJarButton = new javax.swing.JButton(); + addCustomUrlButton = new javax.swing.JButton(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + + setLayout(new java.awt.GridBagLayout()); + + additionalClasspathScrollPane.setViewportView(additionalClasspathList); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(additionalClasspathScrollPane, gridBagConstraints); + + buttonsPanel.setLayout(new java.awt.GridBagLayout()); + + addJarButton.setText(org.openide.util.NbBundle.getMessage(AdditionalClasspathPanel.class, "AdditionalClasspathPanel.addJarButton.text")); // NOI18N + addJarButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addJarButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(addJarButton, gridBagConstraints); + + addCustomUrlButton.setText(org.openide.util.NbBundle.getMessage(AdditionalClasspathPanel.class, "AdditionalClasspathPanel.addCustomUrlButton.text")); // NOI18N + addCustomUrlButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addCustomUrlButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(addCustomUrlButton, gridBagConstraints); + + moveUpButton.setText(org.openide.util.NbBundle.getMessage(AdditionalClasspathPanel.class, "AdditionalClasspathPanel.moveUpButton.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setText(org.openide.util.NbBundle.getMessage(AdditionalClasspathPanel.class, "AdditionalClasspathPanel.moveDownButton.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(moveDownButton, gridBagConstraints); + + removeButton.setText(org.openide.util.NbBundle.getMessage(AdditionalClasspathPanel.class, "AdditionalClasspathPanel.removeButton.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(removeButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + add(buttonsPanel, gridBagConstraints); + }// //GEN-END:initComponents + + private void addJarButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addJarButtonActionPerformed + pathChooser.setSelectedFile(new File("")); + + if (pathChooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) { + return; + } + + File selectedFile = pathChooser.getSelectedFile(); + + final URL selectedJarUrl; + try { + selectedJarUrl = Utilities.toURI(selectedFile).toURL(); + } catch (MalformedURLException ex) { + dialogService.showWarning("Error: invalid file path"); + return; + } + + additionalClasspathModel.addElement(selectedJarUrl); + }//GEN-LAST:event_addJarButtonActionPerformed + + private void addCustomUrlButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addCustomUrlButtonActionPerformed + String newPathString = dialogService.askForString("Path URL:"); + + if (newPathString == null) { + return; + } + + newPathString = newPathString.trim(); + + final URL newPathUrl; + try { + newPathUrl = new URL(newPathString); + } catch (MalformedURLException ex) { + dialogService.showWarning("Invalid URL"); + return; + } + + additionalClasspathModel.addElement(newPathUrl); + }//GEN-LAST:event_addCustomUrlButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + additionalClasspathList.moveUpSelection(); + }//GEN-LAST:event_moveUpButtonActionPerformed + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + additionalClasspathList.moveDownSelection(); + }//GEN-LAST:event_moveDownButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + additionalClasspathList.removeSelection(); + }//GEN-LAST:event_removeButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addCustomUrlButton; + private javax.swing.JButton addJarButton; + private info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList additionalClasspathList; + private javax.swing.JScrollPane additionalClasspathScrollPane; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + // End of variables declaration//GEN-END:variables + + public Collection getAdditionalClassPathUrls() { + return additionalClasspathModel.getItems(); + } + + public void setAdditionalClasspathUrls(Collection additionalClasspathUrls) { + additionalClasspathModel.setItems(additionalClasspathUrls); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptions.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptions.java new file mode 100644 index 0000000..84f17c7 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptions.java @@ -0,0 +1,238 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.helios.collections.general.CollectionItems; +import net.sourceforge.pmd.RulePriority; + +import java.io.Serializable; +import java.net.URL; +import java.util.*; + +/** + * Modifiable implementation of Options + */ +class DefaultOptions implements Options, Serializable, Cloneable { + private static final long serialVersionUID = 7; + + private String targetJavaVersion; + private String sourceFileEncoding; + private String suppressMarker; + private List additionalClassPathUrls; + private List ruleSets; + private boolean useScanMessagesCache; + private boolean showRulePriorityInTasks; + private boolean showDescriptionInTasks; + private boolean showRuleInTasks; + private boolean showRuleSetInTasks; + private boolean showAnnotationsInEditor; + private boolean showAllMessagesInGuardedSections; + private PathFilteringOptions pathFilteringOptions; + private RulePriority minimumPriority; + private String auxiliaryClassPath; + + public DefaultOptions() { + //Just do nothing + } + + @Override + public String getTargetJavaVersion() { + return targetJavaVersion; + } + + public void setTargetJavaVersion(String targetJavaVersion) { + this.targetJavaVersion = targetJavaVersion; + } + + @Override + public String getSourceFileEncoding() { + return sourceFileEncoding; + } + + public void setSourceFileEncoding(String sourceFileEncoding) { + this.sourceFileEncoding = sourceFileEncoding; + } + + @Override + public String getSuppressMarker() { + return suppressMarker; + } + + public void setSuppressMarker(String suppressMarker) { + this.suppressMarker = suppressMarker; + } + + @Override + public Collection getAdditionalClassPathUrls() { + return Collections.unmodifiableCollection(additionalClassPathUrls); + } + + public void setAdditionalClassPathUrls(Collection additionalClassPathUrls) { + this.additionalClassPathUrls = new ArrayList<>(additionalClassPathUrls); + } + + @Override + public Collection getRuleSets() { + return Collections.unmodifiableCollection(ruleSets); + } + + public void setRuleSets(Collection ruleSets) { + this.ruleSets = new ArrayList<>(ruleSets); + } + + @Override + public boolean isUseScanMessagesCache() { + return useScanMessagesCache; + } + + public void setUseScanMessagesCache(boolean useScanMessagesCache) { + this.useScanMessagesCache = useScanMessagesCache; + } + + @Override + public boolean isShowRulePriorityInTasks() { + return showRulePriorityInTasks; + } + + public void setShowRulePriorityInTasks(boolean showRulePriorityInTasks) { + this.showRulePriorityInTasks = showRulePriorityInTasks; + } + + @Override + public boolean isShowDescriptionInTasks() { + return showDescriptionInTasks; + } + + public void setShowDescriptionInTasks(boolean showDescriptionInTasks) { + this.showDescriptionInTasks = showDescriptionInTasks; + } + + @Override + public boolean isShowRuleInTasks() { + return showRuleInTasks; + } + + public void setShowRuleInTasks(boolean showRuleInTasks) { + this.showRuleInTasks = showRuleInTasks; + } + + @Override + public boolean isShowRuleSetInTasks() { + return showRuleSetInTasks; + } + + public void setShowRuleSetInTasks(boolean showRuleSetInTasks) { + this.showRuleSetInTasks = showRuleSetInTasks; + } + + @Override + public boolean isShowAnnotationsInEditor() { + return showAnnotationsInEditor; + } + + public void setShowAnnotationsInEditor(boolean showAnnotationsInEditor) { + this.showAnnotationsInEditor = showAnnotationsInEditor; + } + + @Override + public boolean isShowAllMessagesInGuardedSections() { + return showAllMessagesInGuardedSections; + } + + public void setShowAllMessagesInGuardedSections(boolean showAllMessagesInGuardedSections) { + this.showAllMessagesInGuardedSections = showAllMessagesInGuardedSections; + } + + @Override + public PathFilteringOptions getPathFilteringOptions() { + return pathFilteringOptions; + } + + public void setPathFilteringOptions(PathFilteringOptions pathFilteringOptions) { + this.pathFilteringOptions = pathFilteringOptions; + } + + @Override + public RulePriority getMinimumPriority() { + return minimumPriority; + } + + public void setMinimumPriority(RulePriority minimumPriority) { + this.minimumPriority = minimumPriority; + } + + @Override + public String getAuxiliaryClassPath() { + return auxiliaryClassPath; + } + + public void setAuxiliaryClassPath(String auxiliaryClassPath) { + this.auxiliaryClassPath = auxiliaryClassPath; + } + + + @Override + public Options clone() { + DefaultOptions result; + try { + result = (DefaultOptions) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new IllegalStateException(); + } + + //Manually cloning collections... + result.additionalClassPathUrls = new ArrayList<>(additionalClassPathUrls); + result.ruleSets = new ArrayList<>(ruleSets); + + return result; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Options)) { + return false; + } + + Options other = (Options) obj; + + return Objects.equals(getTargetJavaVersion(), other.getTargetJavaVersion()) + && Objects.equals(getSourceFileEncoding(), other.getSourceFileEncoding()) + && Objects.equals(getSuppressMarker(), other.getSuppressMarker()) + && CollectionItems.equals(getAdditionalClassPathUrls(), other.getAdditionalClassPathUrls()) + && CollectionItems.equals(getRuleSets(), other.getRuleSets()) + && isUseScanMessagesCache() == other.isUseScanMessagesCache() + && isShowRulePriorityInTasks() == other.isShowRulePriorityInTasks() + && isShowDescriptionInTasks() == other.isShowDescriptionInTasks() + && isShowRuleInTasks() == other.isShowRuleInTasks() + && isShowRuleSetInTasks() == other.isShowRuleSetInTasks() + && isShowAnnotationsInEditor() == other.isShowAnnotationsInEditor() + && isShowAllMessagesInGuardedSections() == other.isShowAllMessagesInGuardedSections() + && Objects.equals(getPathFilteringOptions(), other.getPathFilteringOptions()) + && Objects.equals(getMinimumPriority(), other.getMinimumPriority()) + && Objects.equals(getAuxiliaryClassPath(), other.getAuxiliaryClassPath()); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsFactory.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsFactory.java new file mode 100644 index 0000000..65d03dd --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsFactory.java @@ -0,0 +1,110 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.SystemPropertiesService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.pmdscanner.LanguageVersionParser; +import info.gianlucacosta.easypmd7.pmdscanner.StandardRuleSetsCatalog; +import info.gianlucacosta.helios.regex.OsSpecificPathCompositeRegex; +import net.sourceforge.pmd.RulePriority; +import org.openide.util.lookup.ServiceProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default implementation of OptionsFactory + */ +@ServiceProvider(service = OptionsFactory.class) +public class DefaultOptionsFactory implements OptionsFactory { + + private static final Logger logger = Logger.getLogger(DefaultOptionsFactory.class.getName()); + private static final String defaultJavaLanguageVersion = "1.8"; + + private final StandardRuleSetsCatalog standardRulesetsCatalog; + private final SystemPropertiesService systemPropertiesService; + private final LanguageVersionParser languageVersionParser; + + public DefaultOptionsFactory() { + standardRulesetsCatalog = Injector.lookup(StandardRuleSetsCatalog.class); + systemPropertiesService = Injector.lookup(SystemPropertiesService.class); + languageVersionParser = Injector.lookup(LanguageVersionParser.class); + } + + @Override + public Options createDefaultOptions() { + DefaultOptions result = new DefaultOptions(); + + String retrievedJavaVersion = systemPropertiesService.getJavaVersion(); + String javaVersion = retrievedJavaVersion != null ? retrievedJavaVersion : defaultJavaLanguageVersion; + + try { + languageVersionParser.parse(javaVersion); + } catch (IllegalArgumentException ex) { + javaVersion = defaultJavaLanguageVersion; + } + + result.setTargetJavaVersion(javaVersion); + result.setSourceFileEncoding("utf-8"); + result.setSuppressMarker("NOPMD"); + result.setMinimumPriority(RulePriority.MEDIUM); + + result.setAdditionalClassPathUrls(new ArrayList<>()); + + final String[] suggestedDefaultRuleSetFileNames = new String[]{ + "rulesets/java/basic.xml", + "rulesets/java/imports.xml", + "rulesets/java/unusedcode.xml" + }; + + List defaultRuleSets = new ArrayList<>(); + + for (String ruleSetFileName : suggestedDefaultRuleSetFileNames) { + if (standardRulesetsCatalog.containsFileName(ruleSetFileName)) { + defaultRuleSets.add(ruleSetFileName); + } else { + logger.log(Level.WARNING, String.format("The standard ruleset '%s' was not found", ruleSetFileName)); + } + } + + result.setRuleSets(defaultRuleSets); + + result.setUseScanMessagesCache(true); + + result.setShowRulePriorityInTasks(true); + result.setShowAnnotationsInEditor(true); + result.setShowDescriptionInTasks(true); + result.setShowRuleInTasks(false); + result.setShowRuleSetInTasks(false); + result.setShowAllMessagesInGuardedSections(false); + + result.setPathFilteringOptions( + new PathFilteringOptions( + new OsSpecificPathCompositeRegex("^.*\\.java$"), + new OsSpecificPathCompositeRegex())); + + return result; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsService.java new file mode 100644 index 0000000..3f4cbfa --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/DefaultOptionsService.java @@ -0,0 +1,134 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.profiles.ProfileConfiguration; +import info.gianlucacosta.easypmd7.ide.options.profiles.ProfileConfigurationRepository; +import info.gianlucacosta.helios.beans.events.TriggerEvent; +import info.gianlucacosta.helios.beans.events.TriggerListener; +import org.openide.util.lookup.ServiceProvider; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default implementation of OptionsService + */ +@ServiceProvider(service = OptionsService.class) +public class DefaultOptionsService implements OptionsService { + + private static final Logger logger = Logger.getLogger(DefaultOptionsService.class.getName()); + private final TriggerEvent optionsChangedEvent = new TriggerEvent(); + private final Lock readLock; + private final Lock writeLock; + private final Collection optionsVerifiers = new ArrayList<>(); + private Options options; + + public DefaultOptionsService() { + ReadWriteLock optionsLock = new ReentrantReadWriteLock(); + readLock = optionsLock.readLock(); + writeLock = optionsLock.writeLock(); + + ProfileConfigurationRepository profileConfigurationRepository = Injector.lookup(ProfileConfigurationRepository.class); + ProfileConfiguration profileConfiguration = profileConfigurationRepository.getProfileConfiguration(); + options = profileConfiguration.getActiveOptions(); + } + + @Override + public void addOptionsChangedListener(TriggerListener listener) { + writeLock.lock(); + try { + optionsChangedEvent.addListener(listener); + } finally { + writeLock.unlock(); + } + } + + @Override + public void removeOptionsChangedListener(TriggerListener listener) { + writeLock.lock(); + try { + optionsChangedEvent.removeListener(listener); + } finally { + writeLock.unlock(); + } + } + + @Override + public Options getOptions() { + readLock.lock(); + + try { + return options; + } finally { + readLock.unlock(); + } + + } + + @Override + public void setOptions(Options options) { + writeLock.lock(); + try { + Options oldOptions = this.options; + + this.options = options; + + if (!options.equals(oldOptions)) { + logger.log(Level.INFO, "Options changed!"); + + optionsChangedEvent.fire(); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void addOptionsVerifier(OptionsVerifier optionsVerifier) { + optionsVerifiers.add(optionsVerifier); + } + + @Override + public void removeOptionsVerifier(OptionsVerifier optionsVerifier) { + optionsVerifiers.remove(optionsVerifier); + } + + @Override + public void verifyOptions(Options options) throws InvalidOptionsException { + writeLock.lock(); + + try { + for (OptionsVerifier optionsVerifier : optionsVerifiers) { + optionsVerifier.verifyOptions(options); + } + } finally { + writeLock.unlock(); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdOptionsPanelController.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdOptionsPanelController.java new file mode 100644 index 0000000..2156539 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdOptionsPanelController.java @@ -0,0 +1,130 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.profiles.ProfileConfiguration; +import info.gianlucacosta.easypmd7.ide.options.profiles.ProfileConfigurationRepository; +import info.gianlucacosta.easypmd7.pmdscanner.PmdScanner; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; + +import javax.swing.*; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +/** + * Controller underlying the plugin's options panel + */ +@OptionsPanelController.TopLevelRegistration( + id = "info.gianlucacosta.easypmd7.ide.options.EasyPmdOptionsPanelController", + categoryName = "#Option_DisplayName_EasyPmd", + keywords = "#Option_Keywords_EasyPmd", + keywordsCategory = "#Option_KeywordsCategory_EasyPmd", + iconBase = "info/gianlucacosta/easypmd7/mainIcon32.png" +) +public class EasyPmdOptionsPanelController extends OptionsPanelController { + + private static final String EASY_PMD_OPTIONS_NAME_IN_EVENT = "EASYPMD_OPTIONS"; + private final EasyPmdPanel panel = new EasyPmdPanel(); + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private final ProfileConfigurationRepository profileConfigurationRepository; + private final OptionsService optionsService; + private boolean optionsChanged; + + public EasyPmdOptionsPanelController() { + profileConfigurationRepository = Injector.lookup(ProfileConfigurationRepository.class); + optionsService = Injector.lookup(OptionsService.class); + + optionsService.addOptionsVerifier((Options options) -> { + try { + new PmdScanner(options); + } catch (RuntimeException ex) { + throw new InvalidOptionsException(ex); + } + }); + } + + @Override + public void update() { + ProfileConfiguration profileConfiguration = profileConfigurationRepository.getProfileConfiguration(); + + panel.setProfileConfiguration(profileConfiguration); + } + + @Override + public void applyChanges() { + ProfileConfiguration profileConfiguration = panel.getProfileConfiguration(); + profileConfigurationRepository.saveProfileConfiguration(profileConfiguration); + + Options oldOptions = optionsService.getOptions(); + Options newOptions = profileConfiguration.getActiveOptions(); + + optionsService.setOptions(newOptions); + + if (!newOptions.equals(oldOptions)) { + pcs.firePropertyChange(EASY_PMD_OPTIONS_NAME_IN_EVENT, oldOptions, newOptions); + } + } + + @Override + public void cancel() { + //Need not do anything special, if no changes have been persisted yet + } + + @Override + public boolean isValid() { + try { + Options activeOptions = panel.getProfileConfiguration().getActiveOptions(); + optionsService.verifyOptions(activeOptions); + return true; + } catch (InvalidOptionsException ex) { + return false; + } + } + + @Override + public boolean isChanged() { + return optionsChanged; + } + + @Override + public HelpCtx getHelpCtx() { + return new HelpCtx("info.gianlucacosta.easypmd7.options"); + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return panel; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + pcs.addPropertyChangeListener(l); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + pcs.removePropertyChangeListener(l); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.form b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.form new file mode 100644 index 0000000..3d63e94 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.form @@ -0,0 +1,625 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.java new file mode 100644 index 0000000..f218703 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/EasyPmdPanel.java @@ -0,0 +1,803 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.ide.DialogService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.profiles.*; +import info.gianlucacosta.easypmd7.pmdscanner.messagescache.ScanMessagesCache; +import info.gianlucacosta.helios.application.io.CommonQuestionOutcome; +import info.gianlucacosta.helios.product.ProductInfoService; +import info.gianlucacosta.helios.regex.OsSpecificPathCompositeRegex; +import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.RulePriority; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * The plugin's panel shown in the Options dialog + */ +class EasyPmdPanel extends JPanel { + + private final ProductInfoService pluginInfoService; + private final DialogService dialogService; + private final OptionsFactory optionsFactory; + private final OptionsService optionsService; + private final ScanMessagesCache scanMessagesCache; + private ProfileMap profiles; + private String activeProfileName; + private final ActionListener profileComboActionListener; + private boolean refillingProfileCombo; + + EasyPmdPanel() { + pluginInfoService = Injector.lookup(ProductInfoService.class); + optionsService = Injector.lookup(OptionsService.class); + scanMessagesCache = Injector.lookup(ScanMessagesCache.class); + + initComponents(); + + pathFilteringScrollPane.getVerticalScrollBar().setUnitIncrement(300); + + dialogService = Injector.lookup(DialogService.class); + optionsFactory = Injector.lookup(OptionsFactory.class); + + minimumPriorityCombo.setModel(new RulePriorityComboBoxModel()); + + profileComboActionListener = (ActionEvent e) -> { + if (refillingProfileCombo) { + return; + } + + String selectedProfileName = (String) profileCombo.getSelectedItem(); + + String oldProfileName = EasyPmdPanel.this.activeProfileName; + EasyPmdPanel.this.activeProfileName = selectedProfileName; + + updateOptionsControls(oldProfileName); + }; + + ImageIcon pluginIcon = new ImageIcon(getClass().getResource("/info/gianlucacosta/easypmd7/mainIcon128.png")); + + pluginIconPicture.setIcon(pluginIcon); + pluginIconPicture.setText(""); + + pluginTitleLabel.setText( + String.format("%s %s", + pluginInfoService.getName(), + pluginInfoService.getVersion() + ) + ); + + pmdVersionLabel.setText( + String.format("PMD version %s", PMD.VERSION) + ); + } + + private Options getOptions() { + DefaultOptions result = new DefaultOptions(); + + result.setTargetJavaVersion(targetJavaVersionField.getText().trim()); + result.setSourceFileEncoding(sourceFileEncodingField.getText().trim()); + result.setSuppressMarker(suppressMarkerField.getText().trim()); + result.setMinimumPriority((RulePriority) minimumPriorityCombo.getSelectedItem()); + + + result.setAdditionalClassPathUrls(additionalClasspathPanel.getAdditionalClassPathUrls()); + result.setRuleSets(ruleSetsPanel.getRuleSets()); + + result.setUseScanMessagesCache(useScanMessagesCacheCheckBox.isSelected()); + + result.setShowRulePriorityInTasks(showRulePriorityInTasksCheckBox.isSelected()); + result.setShowDescriptionInTasks(showDescriptionInTasksCheckBox.isSelected()); + result.setShowRuleInTasks(showRuleInTasksCheckBox.isSelected()); + result.setShowRuleSetInTasks(showRuleSetInTasksCheckBox.isSelected()); + result.setShowAnnotationsInEditor(showAnnotationsInEditorCheckBox.isSelected()); + result.setShowAllMessagesInGuardedSections(showAllMessagesInGuardedSectionsCheckBox.isSelected()); + + result.setPathFilteringOptions( + new PathFilteringOptions( + new OsSpecificPathCompositeRegex(pathFilteringPanel.getIncludedPathRegexes()), + new OsSpecificPathCompositeRegex(pathFilteringPanel.getExcludedPathRegexes()))); + + result.setAuxiliaryClassPath(auxiliaryPathField.getText().trim()); + + return result; + } + + synchronized ProfileConfiguration getProfileConfiguration() { + updateOptionsInActiveProfile(); + + return new DefaultProfileConfiguration(profiles, activeProfileName); + } + + private void updateOptionsInActiveProfile() { + Options activeOptions = getOptions(); + Profile activeProfile = new DefaultProfile(activeOptions); + + try { + profiles.setProfile(activeProfileName, activeProfile); + } catch (ProfileException ex) { + throw new RuntimeException(ex); + } + } + + private void refillProfileCombo() { + refillingProfileCombo = true; + + try { + profileCombo.removeAllItems(); + for (String profileName : profiles.getProfileNames()) { + profileCombo.addItem(profileName); + } + } finally { + refillingProfileCombo = false; + } + } + + private void updateProfileButtons() { + removeProfileButton.setEnabled(profiles.getProfileNames().size() > 1); + } + + private void updateOptionsControls(String oldProfileName) { + if (activeProfileName.equals(oldProfileName)) { + return; + } + + if (profiles.profileNameExists(oldProfileName)) { + Options oldProfileOptions = getOptions(); + + Profile oldProfile = new DefaultProfile(oldProfileOptions); + try { + profiles.setProfile(oldProfileName, oldProfile); + } catch (ProfileException ex) { + throw new RuntimeException(ex); + } + } + + Profile activeProfile = profiles.getProfile(activeProfileName); + setOptions(activeProfile.getOptions()); + } + + synchronized void setProfileConfiguration(ProfileConfiguration profileConfiguration) { + this.activeProfileName = profileConfiguration.getActiveProfileName(); + + profiles = profileConfiguration.getProfiles(); + + profiles.addProfileNamesChangedListener(() -> { + refillProfileCombo(); + updateProfileButtons(); + }); + + profileCombo.removeActionListener(profileComboActionListener); + refillProfileCombo(); + updateProfileButtons(); + + profileCombo.addActionListener(profileComboActionListener); + profileCombo.setSelectedItem(activeProfileName); + + this.activeProfileName = profileConfiguration.getActiveProfileName(); + Options initialOptions = profileConfiguration.getActiveOptions(); + setOptions(initialOptions); + } + + private void setOptions(Options options) { + targetJavaVersionField.setText(options.getTargetJavaVersion()); + sourceFileEncodingField.setText(options.getSourceFileEncoding()); + suppressMarkerField.setText(options.getSuppressMarker()); + minimumPriorityCombo.setSelectedItem(options.getMinimumPriority()); + + additionalClasspathPanel.setAdditionalClasspathUrls(options.getAdditionalClassPathUrls()); + ruleSetsPanel.setRuleSets(options.getRuleSets()); + + useScanMessagesCacheCheckBox.setSelected(options.isUseScanMessagesCache()); + + showRulePriorityInTasksCheckBox.setSelected(options.isShowRulePriorityInTasks()); + showDescriptionInTasksCheckBox.setSelected(options.isShowDescriptionInTasks()); + showRuleInTasksCheckBox.setSelected(options.isShowRuleInTasks()); + showRuleSetInTasksCheckBox.setSelected(options.isShowRuleSetInTasks()); + showAnnotationsInEditorCheckBox.setSelected(options.isShowAnnotationsInEditor()); + showAllMessagesInGuardedSectionsCheckBox.setSelected(options.isShowAllMessagesInGuardedSections()); + + pathFilteringPanel.setIncludedPathRegexes(options.getPathFilteringOptions().getIncludedPathCompositeRegex().getSubRegexes()); + pathFilteringPanel.setExcludedPathRegexes(options.getPathFilteringOptions().getExcludedPathCompositeRegex().getSubRegexes()); + + auxiliaryPathField.setText(options.getAuxiliaryClassPath()); + } + + private boolean clearScanMessagesScache() { + return scanMessagesCache.clear(); + } + + private void verifyOptions() throws InvalidOptionsException { + optionsService.verifyOptions(getOptions()); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + resetSettingsButton = new javax.swing.JButton(); + verifySettingsButton = new javax.swing.JButton(); + profilePanel = new javax.swing.JPanel(); + profileLabel = new javax.swing.JLabel(); + profileCombo = new javax.swing.JComboBox(); + duplicateProfileButton = new javax.swing.JButton(); + renameProfileButton = new javax.swing.JButton(); + removeProfileButton = new javax.swing.JButton(); + optionsTabbedPane = new javax.swing.JTabbedPane(); + generalPanel = new javax.swing.JPanel(); + targetJavaVersionLabel = new javax.swing.JLabel(); + targetJavaVersionField = new javax.swing.JTextField(); + sourceFileEncodingLabel = new javax.swing.JLabel(); + sourceFileEncodingField = new javax.swing.JTextField(); + suppressMarkerLabel = new javax.swing.JLabel(); + suppressMarkerField = new javax.swing.JTextField(); + minimumPriorityLabel = new javax.swing.JLabel(); + minimumPriorityCombo = new javax.swing.JComboBox(); + mainAdditionalClasspathPanel = new javax.swing.JPanel(); + additionalClasspathPanel = new info.gianlucacosta.easypmd7.ide.options.AdditionalClasspathPanel(); + mainRuleSetsPanel = new javax.swing.JPanel(); + ruleSetsPanel = new info.gianlucacosta.easypmd7.ide.options.RuleSetsPanel(); + cachePanel = new javax.swing.JPanel(); + useScanMessagesCacheCheckBox = new javax.swing.JCheckBox(); + clearScanMessagesCacheButton = new javax.swing.JButton(); + reportingPanel = new javax.swing.JPanel(); + showDescriptionInTasksCheckBox = new javax.swing.JCheckBox(); + showRuleInTasksCheckBox = new javax.swing.JCheckBox(); + showRuleSetInTasksCheckBox = new javax.swing.JCheckBox(); + showAnnotationsInEditorCheckBox = new javax.swing.JCheckBox(); + showAllMessagesInGuardedSectionsCheckBox = new javax.swing.JCheckBox(); + showRulePriorityInTasksCheckBox = new javax.swing.JCheckBox(); + mainPathFilteringPanel = new javax.swing.JPanel(); + pathFilteringScrollPane = new javax.swing.JScrollPane(); + pathFilteringPanel = new info.gianlucacosta.easypmd7.ide.options.PathFilteringPanel(); + miscPanel = new javax.swing.JPanel(); + auxiliaryClassPathLabel = new javax.swing.JLabel(); + auxiliaryPathField = new javax.swing.JTextField(); + infoPanel = new javax.swing.JPanel(); + pluginIconPicture = new javax.swing.JLabel(); + pluginTitleLabel = new javax.swing.JLabel(); + pmdVersionLabel = new javax.swing.JLabel(); + showHomePageButton = new javax.swing.JButton(); + showFacebookPageButton = new javax.swing.JButton(); + + setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(resetSettingsButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.resetSettingsButton.text")); // NOI18N + resetSettingsButton.setPreferredSize(new java.awt.Dimension(101, 30)); + resetSettingsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + resetSettingsButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + add(resetSettingsButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(verifySettingsButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.verifySettingsButton.text")); // NOI18N + verifySettingsButton.setPreferredSize(new java.awt.Dimension(101, 30)); + verifySettingsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + verifySettingsButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + add(verifySettingsButton, gridBagConstraints); + + profilePanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(profileLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.profileLabel.text")); // NOI18N + profilePanel.add(profileLabel, new java.awt.GridBagConstraints()); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 6); + profilePanel.add(profileCombo, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(duplicateProfileButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.duplicateProfileButton.text")); // NOI18N + duplicateProfileButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + duplicateProfileButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 6); + profilePanel.add(duplicateProfileButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(renameProfileButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.renameProfileButton.text")); // NOI18N + renameProfileButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + renameProfileButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 6); + profilePanel.add(renameProfileButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(removeProfileButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.removeProfileButton.text")); // NOI18N + removeProfileButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeProfileButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 4; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 6); + profilePanel.add(removeProfileButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + add(profilePanel, gridBagConstraints); + + optionsTabbedPane.setPreferredSize(new java.awt.Dimension(600, 300)); + + generalPanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(targetJavaVersionLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.targetJavaVersionLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(targetJavaVersionLabel, gridBagConstraints); + + targetJavaVersionField.setText(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.sourceFileEncodingField.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(targetJavaVersionField, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(sourceFileEncodingLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.sourceFileEncodingLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(sourceFileEncodingLabel, gridBagConstraints); + + sourceFileEncodingField.setText(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.sourceFileEncodingField.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(sourceFileEncodingField, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(suppressMarkerLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.suppressMarkerLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(suppressMarkerLabel, gridBagConstraints); + + suppressMarkerField.setText(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.sourceFileEncodingField.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(suppressMarkerField, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(minimumPriorityLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.minimumPriorityLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(minimumPriorityLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + generalPanel.add(minimumPriorityCombo, gridBagConstraints); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.generalPanel.TabConstraints.tabTitle"), generalPanel); // NOI18N + + mainAdditionalClasspathPanel.setLayout(new java.awt.BorderLayout()); + + additionalClasspathPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(7, 7, 7, 7)); + mainAdditionalClasspathPanel.add(additionalClasspathPanel, java.awt.BorderLayout.CENTER); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.mainAdditionalClasspathPanel.TabConstraints.tabTitle"), mainAdditionalClasspathPanel); // NOI18N + + mainRuleSetsPanel.setLayout(new java.awt.BorderLayout()); + + ruleSetsPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(7, 7, 7, 7)); + mainRuleSetsPanel.add(ruleSetsPanel, java.awt.BorderLayout.CENTER); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.mainRuleSetsPanel.TabConstraints.tabTitle"), mainRuleSetsPanel); // NOI18N + + cachePanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(useScanMessagesCacheCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.useScanMessagesCacheCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + cachePanel.add(useScanMessagesCacheCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(clearScanMessagesCacheButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.clearScanMessagesCacheButton.text")); // NOI18N + clearScanMessagesCacheButton.setPreferredSize(new java.awt.Dimension(107, 30)); + clearScanMessagesCacheButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + clearScanMessagesCacheButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + cachePanel.add(clearScanMessagesCacheButton, gridBagConstraints); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.cachePanel.TabConstraints.tabTitle"), cachePanel); // NOI18N + + reportingPanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(showDescriptionInTasksCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showDescriptionInTasksCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showDescriptionInTasksCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showRuleInTasksCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showRuleInTasksCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showRuleInTasksCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showRuleSetInTasksCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showRuleSetInTasksCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showRuleSetInTasksCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showAnnotationsInEditorCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showAnnotationsInEditorCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showAnnotationsInEditorCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showAllMessagesInGuardedSectionsCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showAllMessagesInGuardedSectionsCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showAllMessagesInGuardedSectionsCheckBox, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showRulePriorityInTasksCheckBox, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showRulePriorityInTasksCheckBox.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + reportingPanel.add(showRulePriorityInTasksCheckBox, gridBagConstraints); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.reportingPanel.TabConstraints.tabTitle"), reportingPanel); // NOI18N + + mainPathFilteringPanel.setLayout(new java.awt.BorderLayout()); + + pathFilteringScrollPane.setHorizontalScrollBar(null); + + pathFilteringPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(7, 7, 7, 7)); + pathFilteringScrollPane.setViewportView(pathFilteringPanel); + + mainPathFilteringPanel.add(pathFilteringScrollPane, java.awt.BorderLayout.CENTER); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.mainPathFilteringPanel.TabConstraints.tabTitle"), mainPathFilteringPanel); // NOI18N + + miscPanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(auxiliaryClassPathLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.auxiliaryClassPathLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + miscPanel.add(auxiliaryClassPathLabel, gridBagConstraints); + + auxiliaryPathField.setText(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.auxiliaryPathField.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + miscPanel.add(auxiliaryPathField, gridBagConstraints); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.miscPanel.TabConstraints.tabTitle"), miscPanel); // NOI18N + + infoPanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(pluginIconPicture, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.pluginIconPicture.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.insets = new java.awt.Insets(4, 16, 4, 16); + infoPanel.add(pluginIconPicture, gridBagConstraints); + + pluginTitleLabel.setFont(new java.awt.Font("Tahoma", 1, 24)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(pluginTitleLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.pluginTitleLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.insets = new java.awt.Insets(4, 8, 16, 8); + infoPanel.add(pluginTitleLabel, gridBagConstraints); + + pmdVersionLabel.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(pmdVersionLabel, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.pmdVersionLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.insets = new java.awt.Insets(8, 16, 24, 16); + infoPanel.add(pmdVersionLabel, gridBagConstraints); + + showHomePageButton.setFont(new java.awt.Font("Tahoma", 0, 18)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(showHomePageButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showHomePageButton.text")); // NOI18N + showHomePageButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + showHomePageButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.ipadx = 10; + gridBagConstraints.ipady = 10; + gridBagConstraints.insets = new java.awt.Insets(8, 16, 8, 16); + infoPanel.add(showHomePageButton, gridBagConstraints); + + showFacebookPageButton.setFont(new java.awt.Font("Tahoma", 0, 18)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(showFacebookPageButton, org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.showFacebookPageButton.text")); // NOI18N + showFacebookPageButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + showFacebookPageButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.ipadx = 10; + gridBagConstraints.ipady = 10; + gridBagConstraints.insets = new java.awt.Insets(8, 16, 8, 16); + infoPanel.add(showFacebookPageButton, gridBagConstraints); + + optionsTabbedPane.addTab(org.openide.util.NbBundle.getMessage(EasyPmdPanel.class, "EasyPmdPanel.infoPanel.TabConstraints.tabTitle"), infoPanel); // NOI18N + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + add(optionsTabbedPane, gridBagConstraints); + }// //GEN-END:initComponents + + private void clearScanMessagesCacheButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearScanMessagesCacheButtonActionPerformed + if (clearScanMessagesScache()) { + dialogService.showInfo("The cache has been correctly cleared"); + } else { + dialogService.showWarning("The cache might have been only partially cleared"); + } + }//GEN-LAST:event_clearScanMessagesCacheButtonActionPerformed + + private void verifySettingsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_verifySettingsButtonActionPerformed + try { + verifyOptions(); + dialogService.showInfo("Your EasyPmd options seem to be correct."); + } catch (InvalidOptionsException ex) { + dialogService.showWarning(String.format("The current EasyPmd options appear to be incorrect.\n%s", ex.getMessage())); + } + }//GEN-LAST:event_verifySettingsButtonActionPerformed + + private void resetSettingsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetSettingsButtonActionPerformed + Options defaultOptions = optionsFactory.createDefaultOptions(); + + setOptions(defaultOptions); + + dialogService.showInfo("The default options have been restored in the dialog controls.\nTo save them, please confirm the options dialog."); + }//GEN-LAST:event_resetSettingsButtonActionPerformed + + private void renameProfileButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_renameProfileButtonActionPerformed + String currentName = activeProfileName; + + String newName = dialogService.askForString("New profile name:", currentName); + if (newName == null) { + return; + } + + try { + updateOptionsInActiveProfile(); + profiles.renameProfile(currentName, newName); + } catch (ProfileException ex) { + dialogService.showWarning(ex.getMessage()); + return; + } + + profileCombo.setSelectedItem(newName); + }//GEN-LAST:event_renameProfileButtonActionPerformed + + private void duplicateProfileButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_duplicateProfileButtonActionPerformed + String newName = dialogService.askForString("New profile name:"); + if (newName == null) { + return; + } + + String sourceName = activeProfileName; + + try { + updateOptionsInActiveProfile(); + profiles.duplicateProfile(sourceName, newName); + } catch (ProfileException ex) { + dialogService.showWarning(ex.getMessage()); + return; + } + + profileCombo.setSelectedItem(newName); + }//GEN-LAST:event_duplicateProfileButtonActionPerformed + + private void removeProfileButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeProfileButtonActionPerformed + if (dialogService.askYesNoQuestion("Do you really wish to delete the selected profile?") != CommonQuestionOutcome.YES) { + return; + } + + try { + profiles.removeProfile(activeProfileName); + } catch (ProfileException ex) { + dialogService.showWarning(ex.getMessage()); + return; + } + + profileCombo.setSelectedIndex(0); + }//GEN-LAST:event_removeProfileButtonActionPerformed + + private void showHomePageButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_showHomePageButtonActionPerformed + try { + Desktop.getDesktop().browse(new URI(pluginInfoService.getWebsite())); + } catch (URISyntaxException | IOException ex) { + throw new RuntimeException(ex); + } + }//GEN-LAST:event_showHomePageButtonActionPerformed + + private void showFacebookPageButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_showFacebookPageButtonActionPerformed + try { + Desktop.getDesktop().browse(new URI(pluginInfoService.getFacebookPage())); + } catch (URISyntaxException | IOException ex) { + throw new RuntimeException(ex); + } + }//GEN-LAST:event_showFacebookPageButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private info.gianlucacosta.easypmd7.ide.options.AdditionalClasspathPanel additionalClasspathPanel; + private javax.swing.JLabel auxiliaryClassPathLabel; + private javax.swing.JTextField auxiliaryPathField; + private javax.swing.JPanel cachePanel; + private javax.swing.JButton clearScanMessagesCacheButton; + private javax.swing.JButton duplicateProfileButton; + private javax.swing.JPanel generalPanel; + private javax.swing.JPanel infoPanel; + private javax.swing.JPanel mainAdditionalClasspathPanel; + private javax.swing.JPanel mainPathFilteringPanel; + private javax.swing.JPanel mainRuleSetsPanel; + private javax.swing.JComboBox minimumPriorityCombo; + private javax.swing.JLabel minimumPriorityLabel; + private javax.swing.JPanel miscPanel; + private javax.swing.JTabbedPane optionsTabbedPane; + private info.gianlucacosta.easypmd7.ide.options.PathFilteringPanel pathFilteringPanel; + private javax.swing.JScrollPane pathFilteringScrollPane; + private javax.swing.JLabel pluginIconPicture; + private javax.swing.JLabel pluginTitleLabel; + private javax.swing.JLabel pmdVersionLabel; + private javax.swing.JComboBox profileCombo; + private javax.swing.JLabel profileLabel; + private javax.swing.JPanel profilePanel; + private javax.swing.JButton removeProfileButton; + private javax.swing.JButton renameProfileButton; + private javax.swing.JPanel reportingPanel; + private javax.swing.JButton resetSettingsButton; + private info.gianlucacosta.easypmd7.ide.options.RuleSetsPanel ruleSetsPanel; + private javax.swing.JCheckBox showAllMessagesInGuardedSectionsCheckBox; + private javax.swing.JCheckBox showAnnotationsInEditorCheckBox; + private javax.swing.JCheckBox showDescriptionInTasksCheckBox; + private javax.swing.JButton showFacebookPageButton; + private javax.swing.JButton showHomePageButton; + private javax.swing.JCheckBox showRuleInTasksCheckBox; + private javax.swing.JCheckBox showRulePriorityInTasksCheckBox; + private javax.swing.JCheckBox showRuleSetInTasksCheckBox; + private javax.swing.JTextField sourceFileEncodingField; + private javax.swing.JLabel sourceFileEncodingLabel; + private javax.swing.JTextField suppressMarkerField; + private javax.swing.JLabel suppressMarkerLabel; + private javax.swing.JTextField targetJavaVersionField; + private javax.swing.JLabel targetJavaVersionLabel; + private javax.swing.JCheckBox useScanMessagesCacheCheckBox; + private javax.swing.JButton verifySettingsButton; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/InvalidOptionsException.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/InvalidOptionsException.java new file mode 100644 index 0000000..4af7c2d --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/InvalidOptionsException.java @@ -0,0 +1,47 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +/** + * Exception thrown when the options are invalid + */ +public class InvalidOptionsException extends Exception { + + public InvalidOptionsException() { + } + + public InvalidOptionsException(String message) { + super(message); + } + + public InvalidOptionsException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidOptionsException(Throwable cause) { + super(cause); + } + + public InvalidOptionsException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/Options.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/Options.java new file mode 100644 index 0000000..7170964 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/Options.java @@ -0,0 +1,65 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import net.sourceforge.pmd.RulePriority; + +import java.net.URL; +import java.util.Collection; + +/** + * The plugin's options + */ +public interface Options { + + String getTargetJavaVersion(); + + String getSourceFileEncoding(); + + String getSuppressMarker(); + + Collection getAdditionalClassPathUrls(); + + Collection getRuleSets(); + + boolean isUseScanMessagesCache(); + + boolean isShowRulePriorityInTasks(); + + boolean isShowDescriptionInTasks(); + + boolean isShowRuleInTasks(); + + boolean isShowRuleSetInTasks(); + + boolean isShowAnnotationsInEditor(); + + boolean isShowAllMessagesInGuardedSections(); + + PathFilteringOptions getPathFilteringOptions(); + + RulePriority getMinimumPriority(); + + String getAuxiliaryClassPath(); + + Options clone(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsFactory.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsFactory.java new file mode 100644 index 0000000..9483c90 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsFactory.java @@ -0,0 +1,30 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +/** + * Creates default instances of the plugin's options + */ +public interface OptionsFactory { + + Options createDefaultOptions(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsService.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsService.java new file mode 100644 index 0000000..835070d --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsService.java @@ -0,0 +1,45 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.helios.beans.events.TriggerListener; + +/** + * Provides methods to get/set options, to verify them and to monitor their + * changes + */ +public interface OptionsService { + + void addOptionsChangedListener(TriggerListener listener); + + void removeOptionsChangedListener(TriggerListener listener); + + Options getOptions(); + + void setOptions(Options options); + + void verifyOptions(Options options) throws InvalidOptionsException; + + void addOptionsVerifier(OptionsVerifier optionsVerifier); + + void removeOptionsVerifier(OptionsVerifier optionsVerifier); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsVerifier.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsVerifier.java new file mode 100644 index 0000000..9bf21ee --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/OptionsVerifier.java @@ -0,0 +1,30 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +/** + * Must assert the validity of the passed options + */ +public interface OptionsVerifier { + + void verifyOptions(Options options) throws InvalidOptionsException; +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringOptions.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringOptions.java new file mode 100644 index 0000000..5140203 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringOptions.java @@ -0,0 +1,89 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.helios.regex.CompositeRegex; + +import java.io.Serializable; + +/** + * Options related to path filtering + */ +public class PathFilteringOptions implements Serializable { + + private final CompositeRegex includedPathCompositeRegex; + private final CompositeRegex excludedPathCompositeRegex; + + public PathFilteringOptions(CompositeRegex includedPathCompositeRegex, CompositeRegex excludedPathCompositeRegex) { + this.includedPathCompositeRegex = includedPathCompositeRegex; + this.excludedPathCompositeRegex = excludedPathCompositeRegex; + } + + public CompositeRegex getExcludedPathCompositeRegex() { + return excludedPathCompositeRegex; + } + + public CompositeRegex getIncludedPathCompositeRegex() { + return includedPathCompositeRegex; + } + + private boolean isScanOnlySomePaths() { + return !includedPathCompositeRegex.getSubRegexes().isEmpty(); + } + + private boolean isExcludeSomePaths() { + return !excludedPathCompositeRegex.getSubRegexes().isEmpty(); + } + + public boolean isPathValid(String path) { + if (isScanOnlySomePaths()) { + if (!includedPathCompositeRegex.matches(path)) { + return false; + } + } + + if (isExcludeSomePaths()) { + if (excludedPathCompositeRegex.matches(path)) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PathFilteringOptions)) { + return false; + } + + PathFilteringOptions other = (PathFilteringOptions) obj; + + return includedPathCompositeRegex.equals(other.includedPathCompositeRegex) + && excludedPathCompositeRegex.equals(other.excludedPathCompositeRegex); + } + + @Override + public int hashCode() { + return includedPathCompositeRegex.hashCode() + excludedPathCompositeRegex.hashCode(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.form b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.form new file mode 100644 index 0000000..6f6fee3 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.form @@ -0,0 +1,81 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.java new file mode 100644 index 0000000..8bea950 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/PathFilteringPanel.java @@ -0,0 +1,94 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import javax.swing.*; +import java.util.Collection; + +/** + * Panel dedicated to edit path filtering options + */ +public class PathFilteringPanel extends JPanel { + + /** + * Creates new form PathFilteringPanel + */ + public PathFilteringPanel() { + initComponents(); + } + + public Collection getIncludedPathRegexes() { + return includedPathRegexesPanel.getRegexes(); + } + + public void setIncludedPathRegexes(Collection includedPathRegexes) { + includedPathRegexesPanel.setRegexes(includedPathRegexes); + } + + public Collection getExcludedPathRegexes() { + return excludedPathRegexesPanel.getRegexes(); + } + + public void setExcludedPathRegexes(Collection excludedPathRegexes) { + excludedPathRegexesPanel.setRegexes(excludedPathRegexes); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + includedPathsPanelContainer = new javax.swing.JPanel(); + includedPathRegexesPanel = new info.gianlucacosta.easypmd7.ide.options.regexes.RegexesPanel(); + excludedPathsPanelContainer = new javax.swing.JPanel(); + excludedPathRegexesPanel = new info.gianlucacosta.easypmd7.ide.options.regexes.RegexesPanel(); + + setLayout(new java.awt.GridLayout(2, 1)); + + includedPathsPanelContainer.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(PathFilteringPanel.class, "PathFilteringPanel.includedPathsPanelContainer.border.title"))); // NOI18N + includedPathsPanelContainer.setLayout(new java.awt.BorderLayout()); + + includedPathRegexesPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(7, 7, 7, 7)); + includedPathsPanelContainer.add(includedPathRegexesPanel, java.awt.BorderLayout.CENTER); + + add(includedPathsPanelContainer); + + excludedPathsPanelContainer.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(PathFilteringPanel.class, "PathFilteringPanel.excludedPathsPanelContainer.border.title"))); // NOI18N + excludedPathsPanelContainer.setLayout(new java.awt.BorderLayout()); + + excludedPathRegexesPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(7, 7, 7, 7)); + excludedPathsPanelContainer.add(excludedPathRegexesPanel, java.awt.BorderLayout.CENTER); + + add(excludedPathsPanelContainer); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private info.gianlucacosta.easypmd7.ide.options.regexes.RegexesPanel excludedPathRegexesPanel; + private javax.swing.JPanel excludedPathsPanelContainer; + private info.gianlucacosta.easypmd7.ide.options.regexes.RegexesPanel includedPathRegexesPanel; + private javax.swing.JPanel includedPathsPanelContainer; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/RulePriorityComboBoxModel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RulePriorityComboBoxModel.java new file mode 100644 index 0000000..99b26e9 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RulePriorityComboBoxModel.java @@ -0,0 +1,39 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import net.sourceforge.pmd.RulePriority; + +import javax.swing.*; + +/** + * ComboBoxModel containing PMD's rule priorities. + */ +class RulePriorityComboBoxModel extends DefaultComboBoxModel { + + public RulePriorityComboBoxModel() { + for (RulePriority rulePriority : RulePriority.values()) { + addElement(rulePriority); + } + } + +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.form b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.form new file mode 100644 index 0000000..fd000ce --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.form @@ -0,0 +1,122 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.java new file mode 100644 index 0000000..61660be --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/RuleSetsPanel.java @@ -0,0 +1,231 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options; + +import info.gianlucacosta.easypmd7.ide.DialogService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.pmdscanner.RuleSetWrapper; +import info.gianlucacosta.easypmd7.pmdscanner.StandardRuleSetsCatalog; +import info.gianlucacosta.helios.conversions.CollectionToArrayConverter; +import info.gianlucacosta.helios.product.ProductInfoService; +import info.gianlucacosta.helios.swing.jlist.AdvancedSelectionListModel; +import net.sourceforge.pmd.RuleSet; + +import javax.swing.*; +import java.util.Collection; + +/** + * Panel dedicated to editing rulesets + */ +public class RuleSetsPanel extends JPanel { + + private static final CollectionToArrayConverter rulesetsArrayConverter = new CollectionToArrayConverter<>(RuleSetWrapper.class); + private final AdvancedSelectionListModel ruleSetsModel = new AdvancedSelectionListModel<>(); + private final DialogService dialogService; + private final ProductInfoService pluginInfoService; + private final StandardRuleSetsCatalog standardRulesetsCatalog; + + public RuleSetsPanel() { + initComponents(); + + dialogService = Injector.lookup(DialogService.class); + pluginInfoService = Injector.lookup(ProductInfoService.class); + standardRulesetsCatalog = Injector.lookup(StandardRuleSetsCatalog.class); + + ruleSetsList.setModel(ruleSetsModel); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + ruleSetsScrollPane = new javax.swing.JScrollPane(); + ruleSetsList = new info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList(); + buttonsPanel = new javax.swing.JPanel(); + addStandardRuleSetButton = new javax.swing.JButton(); + addCustomRuleSetButton = new javax.swing.JButton(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + + setLayout(new java.awt.GridBagLayout()); + + ruleSetsScrollPane.setViewportView(ruleSetsList); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(ruleSetsScrollPane, gridBagConstraints); + + buttonsPanel.setLayout(new java.awt.GridBagLayout()); + + addStandardRuleSetButton.setText(org.openide.util.NbBundle.getMessage(RuleSetsPanel.class, "RuleSetsPanel.addStandardRuleSetButton.text")); // NOI18N + addStandardRuleSetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addStandardRuleSetButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(addStandardRuleSetButton, gridBagConstraints); + + addCustomRuleSetButton.setText(org.openide.util.NbBundle.getMessage(RuleSetsPanel.class, "RuleSetsPanel.addCustomRuleSetButton.text")); // NOI18N + addCustomRuleSetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addCustomRuleSetButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(addCustomRuleSetButton, gridBagConstraints); + + moveUpButton.setText(org.openide.util.NbBundle.getMessage(RuleSetsPanel.class, "RuleSetsPanel.moveUpButton.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setText(org.openide.util.NbBundle.getMessage(RuleSetsPanel.class, "RuleSetsPanel.moveDownButton.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(moveDownButton, gridBagConstraints); + + removeButton.setText(org.openide.util.NbBundle.getMessage(RuleSetsPanel.class, "RuleSetsPanel.removeButton.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + buttonsPanel.add(removeButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + add(buttonsPanel, gridBagConstraints); + }// //GEN-END:initComponents + + private void addStandardRuleSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addStandardRuleSetButtonActionPerformed + Collection standardRuleSetWrappers = standardRulesetsCatalog.getRuleSetWrappers(); + + Object userChoice = JOptionPane.showInputDialog( + null, + "Choose a standard rule set:", + pluginInfoService.getName(), + JOptionPane.PLAIN_MESSAGE, + null, + rulesetsArrayConverter.convert(standardRuleSetWrappers), + null); + + if (userChoice == null) { + return; + } + + RuleSet chosenRuleSet = ((RuleSetWrapper) userChoice).getRuleSet(); + + ruleSetsModel.addElement(chosenRuleSet.getFileName()); + }//GEN-LAST:event_addStandardRuleSetButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + ruleSetsList.removeSelection(); + }//GEN-LAST:event_removeButtonActionPerformed + + private void addCustomRuleSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addCustomRuleSetButtonActionPerformed + String newRuleSet = dialogService.askForString("Rule set:"); + + if (newRuleSet == null) { + return; + } + + newRuleSet = newRuleSet.trim(); + + if (newRuleSet.isEmpty()) { + dialogService.showWarning("Invalid rule set"); + } + + ruleSetsModel.addElement(newRuleSet); + }//GEN-LAST:event_addCustomRuleSetButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + ruleSetsList.moveUpSelection(); + }//GEN-LAST:event_moveUpButtonActionPerformed + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + ruleSetsList.moveDownSelection(); + }//GEN-LAST:event_moveDownButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addCustomRuleSetButton; + private javax.swing.JButton addStandardRuleSetButton; + private javax.swing.JPanel buttonsPanel; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + private info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList ruleSetsList; + private javax.swing.JScrollPane ruleSetsScrollPane; + // End of variables declaration//GEN-END:variables + + public Collection getRuleSets() { + return ruleSetsModel.getItems(); + } + + public void setRuleSets(Collection ruleSets) { + ruleSetsModel.setItems(ruleSets); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfile.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfile.java new file mode 100644 index 0000000..124cbb4 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfile.java @@ -0,0 +1,51 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.ide.options.Options; + +import java.io.Serializable; + +/** + * Default implementation of Profile. + */ +public class DefaultProfile implements Profile, Serializable { + + private final Options options; + + public DefaultProfile(Options options) { + this.options = options; + } + + @Override + public Options getOptions() { + return options; + } + + @Override + public Profile clone() { + Options clonedOptions = options.clone(); + + return new DefaultProfile(clonedOptions); + } + +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfiguration.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfiguration.java new file mode 100644 index 0000000..8ae1261 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfiguration.java @@ -0,0 +1,55 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.ide.options.Options; + +import java.io.Serializable; + +/** + * Default implementation of ProfileConfiguration. + */ +public class DefaultProfileConfiguration implements ProfileConfiguration, Serializable { + + private final ProfileMap profileMap; + private final String activeProfileName; + + public DefaultProfileConfiguration(ProfileMap profileMap, String activeProfileName) { + this.profileMap = profileMap; + this.activeProfileName = activeProfileName; + } + + @Override + public ProfileMap getProfiles() { + return profileMap; + } + + @Override + public String getActiveProfileName() { + return activeProfileName; + } + + @Override + public Options getActiveOptions() { + return profileMap.getProfile(activeProfileName).getOptions(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationFactory.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationFactory.java new file mode 100644 index 0000000..38d923a --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationFactory.java @@ -0,0 +1,58 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.Options; +import info.gianlucacosta.easypmd7.ide.options.OptionsFactory; +import org.openide.util.lookup.ServiceProvider; + +/** + * Default implementation of ProfileConfigurationFactory. + */ +@ServiceProvider(service = ProfileConfigurationFactory.class) +public class DefaultProfileConfigurationFactory implements ProfileConfigurationFactory { + + private static final String defaultProfileName = "Default profile"; + private final OptionsFactory optionsFactory; + + public DefaultProfileConfigurationFactory() { + this.optionsFactory = Injector.lookup(OptionsFactory.class); + } + + @Override + public ProfileConfiguration createDefaultProfileConfiguration() { + Options defaultOptions = optionsFactory.createDefaultOptions(); + Profile defaultProfile = new DefaultProfile(defaultOptions); + + ProfileMap defaultProfiles = new DefaultProfileMap(); + + try { + defaultProfiles.setProfile(defaultProfileName, defaultProfile); + } catch (ProfileException ex) { + throw new RuntimeException(ex); + } + + return new DefaultProfileConfiguration(defaultProfiles, defaultProfileName); + } + +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationRepository.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationRepository.java new file mode 100644 index 0000000..0449d56 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileConfigurationRepository.java @@ -0,0 +1,96 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.StorageAreaService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.io.StreamUtils; +import info.gianlucacosta.helios.io.storagearea.StorageArea; +import info.gianlucacosta.helios.io.storagearea.StorageAreaEntry; +import org.openide.util.lookup.ServiceProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default implementation of ProfileConfigurationRepository. + */ +@ServiceProvider(service = ProfileConfigurationRepository.class) +public class DefaultProfileConfigurationRepository implements ProfileConfigurationRepository { + + private static final String PROFILE_CONFIGURATION_ENTRY_NAME = "ProfileConfiguration"; + private static final Logger logger = Logger.getLogger(DefaultProfileConfigurationFactory.class.getName()); + + private final StorageArea storageArea; + private final ProfileConfigurationFactory profileConfigurationFactory; + + private ProfileConfiguration profileConfiguration; + + public DefaultProfileConfigurationRepository() { + profileConfigurationFactory = Injector.lookup(ProfileConfigurationFactory.class); + + StorageAreaService storageAreaService = Injector.lookup(StorageAreaService.class); + storageArea = storageAreaService.getStorageArea(); + + if (storageArea != null) { + try { + StorageAreaEntry profileConfigurationEntry = storageArea.getEntry(PROFILE_CONFIGURATION_ENTRY_NAME); + if (profileConfigurationEntry.exists()) { + try (InputStream profileConfigurationInputStream = profileConfigurationEntry.openInputStream()) { + profileConfiguration = (ProfileConfiguration) StreamUtils.readSingleObjectFromStream(profileConfigurationInputStream); + } + } + } catch (IOException | ClassNotFoundException ex) { + logger.log(Level.SEVERE, "Exception while loading the options", ex); + } + + if (profileConfiguration == null) { + profileConfiguration = profileConfigurationFactory.createDefaultProfileConfiguration(); + } + } + } + + @Override + public synchronized ProfileConfiguration getProfileConfiguration() { + return profileConfiguration; + } + + @Override + public synchronized void saveProfileConfiguration(ProfileConfiguration profileConfiguration) { + this.profileConfiguration = profileConfiguration; + + if (storageArea != null) { + try { + StorageAreaEntry profileConfigurationEntry = storageArea.getEntry(PROFILE_CONFIGURATION_ENTRY_NAME); + + try (OutputStream profileConfigurationOutputStream = profileConfigurationEntry.openOutputStream()) { + StreamUtils.writeSingleObjectToStream(profileConfigurationOutputStream, profileConfiguration); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, "Error while saving the options", ex); + } + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileMap.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileMap.java new file mode 100644 index 0000000..73c5ea4 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/DefaultProfileMap.java @@ -0,0 +1,125 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.helios.beans.events.TriggerEvent; +import info.gianlucacosta.helios.beans.events.TriggerListener; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation of ProfileMap. + */ +public class DefaultProfileMap implements ProfileMap, Serializable { + + private final TriggerEvent profileNamesChangedEvent = new TriggerEvent(); + private final Map profiles = new HashMap<>(); + + @Override + public void setProfile(String profileName, Profile profile) throws ProfileException { + if (profile == null) { + throw new IllegalArgumentException(); + } + + boolean profileNamesChanged = !profiles.containsKey(profileName); + + profiles.put(profileName, profile); + + if (profileNamesChanged) { + profileNamesChangedEvent.fire(); + } + } + + @Override + public Collection getProfileNames() { + return profiles.keySet(); + } + + @Override + public Profile getProfile(String profileName) { + return profiles.get(profileName); + } + + @Override + public void duplicateProfile(String sourceName, String targetName) throws ProfileException { + if (profiles.containsKey(targetName)) { + throw new ProfileException(String.format("A profile named '%s' already exists", targetName)); + } + + Profile sourceProfile = profiles.get(sourceName); + if (sourceProfile == null) { + throw new IllegalArgumentException(); + } + + Profile targetProfile = sourceProfile.clone(); + profiles.put(targetName, targetProfile); + profileNamesChangedEvent.fire(); + } + + @Override + public void renameProfile(String oldName, String newName) throws ProfileException { + if (oldName.equals(newName)) { + return; + } + + if (profiles.containsKey(newName)) { + throw new ProfileException(String.format("A profile named '%s' already exists", newName)); + } + + Profile profile = profiles.get(oldName); + if (profile == null) { + throw new IllegalArgumentException(); + } + + profiles.put(newName, profile); + profiles.remove(oldName); + profileNamesChangedEvent.fire(); + } + + @Override + public void removeProfile(String name) throws ProfileException { + if (!profiles.containsKey(name)) { + return; + } + + profiles.remove(name); + profileNamesChangedEvent.fire(); + } + + @Override + public void addProfileNamesChangedListener(TriggerListener listener) { + profileNamesChangedEvent.addListener(listener); + } + + @Override + public void removeProfileNamesChangedListener(TriggerListener listener) { + profileNamesChangedEvent.removeListener(listener); + } + + @Override + public boolean profileNameExists(String profileName) { + return profiles.containsKey(profileName); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/Profile.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/Profile.java new file mode 100644 index 0000000..b458c56 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/Profile.java @@ -0,0 +1,34 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.ide.options.Options; + +/** + * Stores a dedicated set of options. + */ +public interface Profile { + + Options getOptions(); + + Profile clone(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfiguration.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfiguration.java new file mode 100644 index 0000000..3b972cc --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfiguration.java @@ -0,0 +1,36 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.easypmd7.ide.options.Options; + +/** + * The overall configuration of profiles and current active profile. + */ +public interface ProfileConfiguration { + + ProfileMap getProfiles(); + + String getActiveProfileName(); + + Options getActiveOptions(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationFactory.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationFactory.java new file mode 100644 index 0000000..34a7a22 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationFactory.java @@ -0,0 +1,30 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +/** + * A factory for creating ProfileConfiguration objects. + */ +public interface ProfileConfigurationFactory { + + ProfileConfiguration createDefaultProfileConfiguration(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationRepository.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationRepository.java new file mode 100644 index 0000000..510fa88 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileConfigurationRepository.java @@ -0,0 +1,32 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +/** + * Loads and saves ProfileConfiguration objects. + */ +public interface ProfileConfigurationRepository { + + ProfileConfiguration getProfileConfiguration(); + + void saveProfileConfiguration(ProfileConfiguration profileConfiguration); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileException.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileException.java new file mode 100644 index 0000000..bafb613 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileException.java @@ -0,0 +1,36 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +/** + * Exception related to the management of Profile objects. + */ +public class ProfileException extends Exception { + + public ProfileException(String message) { + super(message); + } + + public ProfileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileMap.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileMap.java new file mode 100644 index 0000000..56ae0cf --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/profiles/ProfileMap.java @@ -0,0 +1,50 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.profiles; + +import info.gianlucacosta.helios.beans.events.TriggerListener; + +import java.util.Collection; + +/** + * A dedicated map-like object for enumerating profiles. + */ +public interface ProfileMap { + + Collection getProfileNames(); + + void setProfile(String profileName, Profile profile) throws ProfileException; + + boolean profileNameExists(String profileName); + + Profile getProfile(String profileName); + + void duplicateProfile(String sourceName, String targetName) throws ProfileException; + + void renameProfile(String oldName, String newName) throws ProfileException; + + void removeProfile(String name) throws ProfileException; + + void addProfileNamesChangedListener(TriggerListener listener); + + void removeProfileNamesChangedListener(TriggerListener listener); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/DefaultRegexTemplateSelectionDialog.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/DefaultRegexTemplateSelectionDialog.java new file mode 100644 index 0000000..33b3c15 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/DefaultRegexTemplateSelectionDialog.java @@ -0,0 +1,65 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.helios.product.ProductInfoService; +import org.openide.util.lookup.ServiceProvider; + +import javax.swing.*; +import java.util.Arrays; +import java.util.Collection; + +/** + * Default implementation of RegexTemplateSelectionDialog + */ +@ServiceProvider(service = RegexTemplateSelectionDialog.class) +public class DefaultRegexTemplateSelectionDialog implements RegexTemplateSelectionDialog { + + private static final RegexTemplate[] regexTemplates; + + static { + Collection foundRegexTemplates = Injector.lookupAll(RegexTemplate.class); + + regexTemplates = foundRegexTemplates.toArray(new RegexTemplate[0]); + Arrays.sort(regexTemplates); + } + + private final ProductInfoService pluginInfoService; + + public DefaultRegexTemplateSelectionDialog() { + pluginInfoService = Injector.lookup(ProductInfoService.class); + } + + @Override + public RegexTemplate askForRegexTemplate() { + RegexTemplate chosenTemplate = (RegexTemplate) JOptionPane.showInputDialog( + null, + "Please, choose a predefined regular expression:", + pluginInfoService.getName(), + JOptionPane.OK_CANCEL_OPTION | JOptionPane.PLAIN_MESSAGE, + null, + regexTemplates, + null); + return chosenTemplate; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplate.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplate.java new file mode 100644 index 0000000..37d0d25 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplate.java @@ -0,0 +1,42 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes; + +/** + * A template used to create a regular expression + */ +public abstract class RegexTemplate implements Comparable { + + public abstract String getDescription(); + + public abstract String getRegex(); + + @Override + public int compareTo(RegexTemplate other) { + return getDescription().compareTo(other.getDescription()); + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplateSelectionDialog.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplateSelectionDialog.java new file mode 100644 index 0000000..49cdd92 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexTemplateSelectionDialog.java @@ -0,0 +1,30 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes; + +/** + * Dialog for choosing a regex template + */ +public interface RegexTemplateSelectionDialog { + + RegexTemplate askForRegexTemplate(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.form b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.form new file mode 100644 index 0000000..d1f37c6 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.form @@ -0,0 +1,122 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.java new file mode 100644 index 0000000..1bcb851 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/RegexesPanel.java @@ -0,0 +1,238 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes; + +import info.gianlucacosta.easypmd7.ide.DialogService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.helios.swing.jlist.AdvancedSelectionListModel; + +import javax.swing.*; +import java.util.Collection; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Panel for managing regular expressions + */ +public class RegexesPanel extends JPanel { + + private final AdvancedSelectionListModel regexesModel = new AdvancedSelectionListModel<>(); + private final DialogService dialogService; + private final RegexTemplateSelectionDialog regexTemplateSelectionDialog; + + /** + * Creates new form RegexPanel + */ + public RegexesPanel() { + initComponents(); + + dialogService = Injector.lookup(DialogService.class); + regexTemplateSelectionDialog = Injector.lookup(RegexTemplateSelectionDialog.class); + + regexesList.setModel(regexesModel); + } + + public Collection getRegexes() { + return regexesModel.getItems(); + } + + public void setRegexes(Collection regexes) { + regexesModel.setItems(regexes); + } + + private String processInputRegex(String regex) { + regex = regex.trim(); + + try { + Pattern.compile(regex); + } catch (PatternSyntaxException ex) { + dialogService.showWarning("Invalid regular expression"); + return null; + } + + return regex; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + regexesScrollPane = new javax.swing.JScrollPane(); + regexesList = new info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList(); + includedPathsButtonsPanel = new javax.swing.JPanel(); + addPredefinedPathButton = new javax.swing.JButton(); + addCustomPathButton = new javax.swing.JButton(); + moveUpPathButton = new javax.swing.JButton(); + moveDownPathButton = new javax.swing.JButton(); + removePathButton = new javax.swing.JButton(); + + setLayout(new java.awt.BorderLayout()); + + regexesScrollPane.setViewportView(regexesList); + + add(regexesScrollPane, java.awt.BorderLayout.CENTER); + + includedPathsButtonsPanel.setLayout(new java.awt.GridBagLayout()); + + addPredefinedPathButton.setText(org.openide.util.NbBundle.getMessage(RegexesPanel.class, "RegexesPanel.addPredefinedPathButton.text")); // NOI18N + addPredefinedPathButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addPredefinedPathButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + includedPathsButtonsPanel.add(addPredefinedPathButton, gridBagConstraints); + + addCustomPathButton.setText(org.openide.util.NbBundle.getMessage(RegexesPanel.class, "RegexesPanel.addCustomPathButton.text")); // NOI18N + addCustomPathButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addCustomPathButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + includedPathsButtonsPanel.add(addCustomPathButton, gridBagConstraints); + + moveUpPathButton.setText(org.openide.util.NbBundle.getMessage(RegexesPanel.class, "RegexesPanel.moveUpPathButton.text")); // NOI18N + moveUpPathButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpPathButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + includedPathsButtonsPanel.add(moveUpPathButton, gridBagConstraints); + + moveDownPathButton.setText(org.openide.util.NbBundle.getMessage(RegexesPanel.class, "RegexesPanel.moveDownPathButton.text")); // NOI18N + moveDownPathButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownPathButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + includedPathsButtonsPanel.add(moveDownPathButton, gridBagConstraints); + + removePathButton.setText(org.openide.util.NbBundle.getMessage(RegexesPanel.class, "RegexesPanel.removePathButton.text")); // NOI18N + removePathButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removePathButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(7, 7, 7, 7); + includedPathsButtonsPanel.add(removePathButton, gridBagConstraints); + + add(includedPathsButtonsPanel, java.awt.BorderLayout.LINE_END); + }// //GEN-END:initComponents + + private void addPredefinedPathButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addPredefinedPathButtonActionPerformed + while (true) { + RegexTemplate regexTemplate = regexTemplateSelectionDialog.askForRegexTemplate(); + + if (regexTemplate == null) { + return; + } + + String newPathRegex = regexTemplate.getRegex(); + + if (newPathRegex == null) { + return; + } + + newPathRegex = processInputRegex(newPathRegex); + + if (newPathRegex == null || newPathRegex.isEmpty()) { + continue; + } + + regexesModel.addElement(newPathRegex); + + break; + } + }//GEN-LAST:event_addPredefinedPathButtonActionPerformed + + private void addCustomPathButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addCustomPathButtonActionPerformed + while (true) { + String newPathRegex = dialogService.askForString("Regular expression:"); + + if (newPathRegex == null) { + return; + } + + newPathRegex = processInputRegex(newPathRegex); + + if (newPathRegex == null || newPathRegex.isEmpty()) { + continue; + } + + regexesModel.addElement(newPathRegex); + + break; + } + }//GEN-LAST:event_addCustomPathButtonActionPerformed + + private void moveUpPathButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpPathButtonActionPerformed + regexesList.moveUpSelection(); + }//GEN-LAST:event_moveUpPathButtonActionPerformed + + private void moveDownPathButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownPathButtonActionPerformed + regexesList.moveDownSelection(); + }//GEN-LAST:event_moveDownPathButtonActionPerformed + + private void removePathButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removePathButtonActionPerformed + regexesList.removeSelection(); + }//GEN-LAST:event_removePathButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addCustomPathButton; + private javax.swing.JButton addPredefinedPathButton; + private javax.swing.JPanel includedPathsButtonsPanel; + private javax.swing.JButton moveDownPathButton; + private javax.swing.JButton moveUpPathButton; + private info.gianlucacosta.helios.swing.jlist.AdvancedSelectionJList regexesList; + private javax.swing.JScrollPane regexesScrollPane; + private javax.swing.JButton removePathButton; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/SingleStringParamRegexTemplate.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/SingleStringParamRegexTemplate.java new file mode 100644 index 0000000..3ce9b9b --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/SingleStringParamRegexTemplate.java @@ -0,0 +1,65 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes; + +import info.gianlucacosta.easypmd7.ide.DialogService; +import info.gianlucacosta.easypmd7.ide.Injector; + +/** + * Regex template requiring just a single string parameter + */ +public abstract class SingleStringParamRegexTemplate extends RegexTemplate { + + private final DialogService dialogService; + + public SingleStringParamRegexTemplate() { + dialogService = Injector.lookup(DialogService.class); + } + + protected abstract String getPromptMessage(); + + protected abstract String getDefaultValue(); + + protected abstract String getRegex(String stringParam); + + @Override + public String getRegex() { + while (true) { + String stringParam = dialogService.askForString(getPromptMessage(), getDefaultValue()); + + if (stringParam == null) { + return null; + } + + stringParam = stringParam.trim(); + if (stringParam.isEmpty()) { + continue; + } + + try { + return getRegex(stringParam); + } catch (RuntimeException ex) { + dialogService.showWarning(ex.getMessage()); + } + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/AncestorDirectoryRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/AncestorDirectoryRegex.java new file mode 100644 index 0000000..947dc98 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/AncestorDirectoryRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by ancestor directory + */ +@ServiceProvider(service = RegexTemplate.class) +public class AncestorDirectoryRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by ancestor directory"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the ancestor directory pattern:"; + } + + @Override + protected String getDefaultValue() { + return "Simple directory or composite path (e.g: parent1/parent2/parent3)"; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("^(.*/)?%s/.+$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/CaseInsensitiveRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/CaseInsensitiveRegex.java new file mode 100644 index 0000000..e9ec834 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/CaseInsensitiveRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Case-insensitive regex + */ +@ServiceProvider(service = RegexTemplate.class) +public class CaseInsensitiveRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by case insensitive regular expression"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit a regular expression to process as case insensitive:"; + } + + @Override + protected String getDefaultValue() { + return ""; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("(?i)%s", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileExtensionRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileExtensionRegex.java new file mode 100644 index 0000000..eb02de7 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileExtensionRegex.java @@ -0,0 +1,57 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by file extension + */ +@ServiceProvider(service = RegexTemplate.class) +public class FileExtensionRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by file extension"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the file extension for the filter:"; + } + + @Override + protected String getDefaultValue() { + return ".java"; + } + + @Override + protected String getRegex(String stringParam) { + if (!stringParam.startsWith(".")) { + stringParam = "." + stringParam; + } + + return String.format("^.*\\%s$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameRegex.java new file mode 100644 index 0000000..de81fd7 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by file name (including the extension) + */ +@ServiceProvider(service = RegexTemplate.class) +public class FileNameRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by file name (including the extension)"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the file name to filter (with extension):"; + } + + @Override + protected String getDefaultValue() { + return ""; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("^(.*/)?%s$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameWithoutExtensionRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameWithoutExtensionRegex.java new file mode 100644 index 0000000..373e42c --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FileNameWithoutExtensionRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by file name (excluding the extension) + */ +@ServiceProvider(service = RegexTemplate.class) +public class FileNameWithoutExtensionRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by file name (ignoring any extension)"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the file name to filter (without extension):"; + } + + @Override + protected String getDefaultValue() { + return ""; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("^(.*/)?%s(\\.[^/]*)*$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FullPathRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FullPathRegex.java new file mode 100644 index 0000000..7a3ac99 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/FullPathRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by full path + */ +@ServiceProvider(service = RegexTemplate.class) +public class FullPathRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by full path"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the full path for the filter:"; + } + + @Override + protected String getDefaultValue() { + return ""; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("^%s$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/ParentDirectoryRegex.java b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/ParentDirectoryRegex.java new file mode 100644 index 0000000..880c909 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/options/regexes/predefined/ParentDirectoryRegex.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.options.regexes.predefined; + +import info.gianlucacosta.easypmd7.ide.options.regexes.RegexTemplate; +import info.gianlucacosta.easypmd7.ide.options.regexes.SingleStringParamRegexTemplate; +import org.openide.util.lookup.ServiceProvider; + +/** + * Regex filtering by parent directory + */ +@ServiceProvider(service = RegexTemplate.class) +public class ParentDirectoryRegex extends SingleStringParamRegexTemplate { + + @Override + public String getDescription() { + return "Filter by parent directory"; + } + + @Override + protected String getDefaultValue() { + return "Simple directory or composite path (e.g: parent1/parent2/parent3)"; + } + + @Override + protected String getPromptMessage() { + return "Please, digit the parent directory pattern:"; + } + + @Override + protected String getRegex(String stringParam) { + return String.format("^(.*/)?%s/[^/]+$", stringParam); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/ide/tasklist/ScanMessageTaskList.java b/src/main/java/info/gianlucacosta/easypmd7/ide/tasklist/ScanMessageTaskList.java new file mode 100644 index 0000000..f773d3a --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/ide/tasklist/ScanMessageTaskList.java @@ -0,0 +1,47 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.ide.tasklist; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessage; +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; +import org.netbeans.spi.tasklist.Task; +import org.openide.filesystems.FileObject; + +import java.util.ArrayList; + +/** + * A list of tasks, generated from a list of scan messages, related to a + * FileObject + */ +public class ScanMessageTaskList extends ArrayList { + + public ScanMessageTaskList() { + //Just do nothing + } + + public ScanMessageTaskList(FileObject fileObject, ScanMessageList scanMessages) { + for (ScanMessage scanMessage : scanMessages) { + Task violationTask = Task.create(fileObject, scanMessage.getTaskType(), scanMessage.getTaskText(), scanMessage.getLineNumber()); + add(violationTask); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/io/StreamUtils.java b/src/main/java/info/gianlucacosta/easypmd7/io/StreamUtils.java new file mode 100644 index 0000000..77dc8f2 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/io/StreamUtils.java @@ -0,0 +1,46 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.io; + +import java.io.*; + +/** + * Stream-related utilities + */ +public class StreamUtils { + + private StreamUtils() { + } + + public static Object readSingleObjectFromStream(InputStream sourceStream) throws IOException, ClassNotFoundException { + try (ObjectInputStream inputStream = new ObjectInputStream(new BufferedInputStream(sourceStream))) { + + return inputStream.readObject(); + } + } + + public static void writeSingleObjectToStream(OutputStream targetStream, Object obj) throws IOException { + try (ObjectOutputStream outputStream = new ObjectOutputStream(new BufferedOutputStream(targetStream))) { + outputStream.writeObject(obj); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/CacheBasedLinkedPmdScanningStrategy.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/CacheBasedLinkedPmdScanningStrategy.java new file mode 100644 index 0000000..c4fb123 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/CacheBasedLinkedPmdScanningStrategy.java @@ -0,0 +1,58 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.Options; +import info.gianlucacosta.easypmd7.pmdscanner.messagescache.ScanMessagesCache; + +import java.io.File; + +/** + * Scanning strategy looking for cached scan messages: if they are missing, a + * default PMD scan is performed. + */ +class CacheBasedLinkedPmdScanningStrategy extends LinkedPmdScanningStrategy { + + private final ScanMessagesCache scanMessagesCache; + + public CacheBasedLinkedPmdScanningStrategy(Options options) { + super(options); + + scanMessagesCache = Injector.lookup(ScanMessagesCache.class); + } + + @Override + public ScanMessageList scanFile(File file) { + final ScanMessageList cachedScanMessages = scanMessagesCache.getScanMessagesFor(file); + + if (cachedScanMessages != null) { + return cachedScanMessages; + } + + final ScanMessageList scanMessages = super.scanFile(file); + + scanMessagesCache.putScanMessagesFor(file, scanMessages); + + return scanMessages; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultLanguageVersionParser.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultLanguageVersionParser.java new file mode 100644 index 0000000..9ea0d23 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultLanguageVersionParser.java @@ -0,0 +1,70 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.LanguageVersionHandler; +import org.openide.util.lookup.ServiceProvider; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Default implementation of LanguageVersionParser. It currently only supports + * Java. + */ +@ServiceProvider(service = LanguageVersionParser.class) +public class DefaultLanguageVersionParser implements LanguageVersionParser { + + private static final Pattern javaVersionPattern = Pattern.compile("^(\\d)\\.(\\d)$"); + + @Override + public LanguageVersion parse(String languageVersionString) { + Matcher javaVersionMatcher = javaVersionPattern.matcher(languageVersionString); + + if (!javaVersionMatcher.matches()) { + throw new IllegalArgumentException("Unsupported target Java version"); + } + + String majorVersion = javaVersionMatcher.group(1); + String minorVersion = javaVersionMatcher.group(2); + String languageHandlerClassName = String.format("net.sourceforge.pmd.lang.java.Java%s%sHandler", majorVersion, minorVersion); + + try { + Class languageHandlerClass = (Class) Class.forName(languageHandlerClassName); + + LanguageVersionHandler languageHandler = languageHandlerClass.newInstance(); + + Language javaLanguage = LanguageRegistry.getLanguage("Java"); + + if (javaLanguage == null) { + throw new IllegalStateException("Cannot find Java in PMD's language registry"); + } + + return new LanguageVersion(javaLanguage, languageVersionString, languageHandler); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | IllegalArgumentException ex) { + throw new IllegalArgumentException("Unsupported target Java version"); + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultStandardRuleSetsCatalog.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultStandardRuleSetsCatalog.java new file mode 100644 index 0000000..920a496 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/DefaultStandardRuleSetsCatalog.java @@ -0,0 +1,71 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.RuleSetFactory; +import net.sourceforge.pmd.RuleSetNotFoundException; +import org.openide.util.lookup.ServiceProvider; + +import java.util.*; + +/** + * Default implementation of StandardRuleSetsCatalog + */ +@ServiceProvider(service = StandardRuleSetsCatalog.class) +public class DefaultStandardRuleSetsCatalog implements StandardRuleSetsCatalog { + + private final List wrappers = new ArrayList<>(); + + public DefaultStandardRuleSetsCatalog() { + RuleSetFactory ruleSetFactory = new RuleSetFactory(); + + try { + Iterator ruleSetsIterator = ruleSetFactory.getRegisteredRuleSets(); + + while (ruleSetsIterator.hasNext()) { + RuleSet ruleSet = ruleSetsIterator.next(); + wrappers.add(new RuleSetWrapper(ruleSet)); + } + } catch (RuleSetNotFoundException ex) { + throw new RuntimeException("Error while initializing the list of PMD's standard rule sets", ex); + } + } + + @Override + public Collection getRuleSetWrappers() { + return Collections.unmodifiableCollection(wrappers); + } + + @Override + public boolean containsFileName(String ruleSetFileName) { + for (RuleSetWrapper wrapper : wrappers) { + RuleSet ruleSet = wrapper.getRuleSet(); + + if (ruleSet.getFileName().equals(ruleSetFileName)) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LanguageVersionParser.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LanguageVersionParser.java new file mode 100644 index 0000000..791255b --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LanguageVersionParser.java @@ -0,0 +1,32 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import net.sourceforge.pmd.lang.LanguageVersion; + +/** + * Creates a LanguageVersion by parsing a version string + */ +public interface LanguageVersionParser { + + LanguageVersion parse(String versionString); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LinkedPmdScanningStrategy.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LinkedPmdScanningStrategy.java new file mode 100644 index 0000000..cbc909d --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/LinkedPmdScanningStrategy.java @@ -0,0 +1,130 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.Options; +import net.sourceforge.pmd.*; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.dfa.report.ReportTree; + +import java.io.*; +import java.util.Collection; +import java.util.Iterator; + +/** + * Scans a file using PMD + */ +class LinkedPmdScanningStrategy implements PmdScannerStrategy { + + private final LanguageVersionParser languageVersionParser; + private final PMD pmd; + private final RuleSets ruleSets; + private final String sourceFileEncoding; + + public LinkedPmdScanningStrategy(Options options) { + languageVersionParser = Injector.lookup(LanguageVersionParser.class); + + ClassLoader pmdBasedClassLoader = PmdBasedClassLoader.create(options.getAdditionalClassPathUrls()); + + RuleSetFactory rulesetFactory = new RuleSetFactory(); + + String ruleSetsString = buildRuleSetsString(options.getRuleSets()); + + try { + rulesetFactory.setClassLoader(pmdBasedClassLoader); + rulesetFactory.setMinimumPriority(options.getMinimumPriority()); + ruleSets = rulesetFactory.createRuleSets(ruleSetsString); + } catch (RuleSetNotFoundException ex) { + throw new RuntimeException(ex); + } + + LanguageVersion languageVersion = languageVersionParser.parse(options.getTargetJavaVersion()); + sourceFileEncoding = options.getSourceFileEncoding(); + + pmd = new PMD(); + PMDConfiguration pmdConfiguration = pmd.getConfiguration(); + pmdConfiguration.setDefaultLanguageVersion(languageVersion); + pmdConfiguration.setSuppressMarker(options.getSuppressMarker()); + pmdConfiguration.setClassLoader(pmdBasedClassLoader); + pmdConfiguration.setMinimumPriority(options.getMinimumPriority()); + + String auxiliaryClassPath = options.getAuxiliaryClassPath(); + + if (auxiliaryClassPath != null && !auxiliaryClassPath.isEmpty()) { + try { + pmdConfiguration.prependClasspath(auxiliaryClassPath); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } + + private String buildRuleSetsString(Collection ruleSets) { + StringBuilder result = new StringBuilder(); + + Iterator iterator = ruleSets.iterator(); + + while (iterator.hasNext()) { + String ruleSet = iterator.next(); + result.append(ruleSet); + + if (iterator.hasNext()) { + result.append(","); + } + } + + return result.toString(); + } + + @Override + public ScanMessageList scanFile(File file) { + String filePath = file.getAbsolutePath(); + + Report report = new Report(); + + RuleContext ruleContext = new RuleContext(); + ruleContext.setReport(report); + ruleContext.setSourceCodeFilename(filePath); + + ScanMessageList scanMessages = new ScanMessageList(); + + try { + try (Reader reader = new InputStreamReader(new FileInputStream(filePath), sourceFileEncoding)) { + pmd.getSourceCodeProcessor().processSourceCode(reader, ruleSets, ruleContext); + } + + ReportTree violationTree = report.getViolationTree(); + + Iterator violationsIterator = violationTree.iterator(); + while (violationsIterator.hasNext()) { + RuleViolation violation = violationsIterator.next(); + scanMessages.add(new ScanViolation(violation)); + } + + } catch (IOException | PMDException ex) { + scanMessages.add(new ScanError(ex)); + } + + return scanMessages; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/NoOpPmdScannerStrategy.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/NoOpPmdScannerStrategy.java new file mode 100644 index 0000000..4449aa8 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/NoOpPmdScannerStrategy.java @@ -0,0 +1,36 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import java.io.File; + +/** + * Scanning strategy that does just nothing. + */ +public class NoOpPmdScannerStrategy implements PmdScannerStrategy { + + @Override + public ScanMessageList scanFile(File file) { + return new ScanMessageList(); + } + +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdBasedClassLoader.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdBasedClassLoader.java new file mode 100644 index 0000000..884d4a3 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdBasedClassLoader.java @@ -0,0 +1,47 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.helios.conversions.CollectionToArrayConverter; +import net.sourceforge.pmd.PMD; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; + +/** + * ClassLoader that firstly tries to load classes and resources using PMD's + * class loader as its parent + */ +class PmdBasedClassLoader extends URLClassLoader { + + public static PmdBasedClassLoader create(Collection additionalUrls) { + ClassLoader pmdClassLoader = PMD.class.getClassLoader(); + + CollectionToArrayConverter urlsToArrayConverter = new CollectionToArrayConverter<>(URL.class); + return new PmdBasedClassLoader(urlsToArrayConverter.convert(additionalUrls), pmdClassLoader); + } + + private PmdBasedClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScanner.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScanner.java new file mode 100644 index 0000000..84c0d60 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScanner.java @@ -0,0 +1,57 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.easypmd7.ide.options.Options; + +import java.io.File; + +/** + * Scans files using PMD, returning a ScanMessageList for each scanned file + */ +public class PmdScanner { + + private final PmdScannerStrategy strategy; + + public PmdScanner(Options options) { + if (options.getRuleSets().isEmpty()) { + strategy = new NoOpPmdScannerStrategy(); + return; + } + + if (options.isUseScanMessagesCache()) { + strategy = new CacheBasedLinkedPmdScanningStrategy(options); + } else { + strategy = new LinkedPmdScanningStrategy(options); + } + } + + public ScanMessageList scanFile(File file) { + try { + return strategy.scanFile(file); + } catch (RuntimeException ex) { + ScanMessageList result = new ScanMessageList(); + result.add(new ScanError(ex)); + return result; + } + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScannerStrategy.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScannerStrategy.java new file mode 100644 index 0000000..57b3906 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/PmdScannerStrategy.java @@ -0,0 +1,32 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import java.io.File; + +/** + * Strategy used by the PMD scanner to scan files + */ +interface PmdScannerStrategy { + + ScanMessageList scanFile(File file); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/RuleSetWrapper.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/RuleSetWrapper.java new file mode 100644 index 0000000..cfbaa2c --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/RuleSetWrapper.java @@ -0,0 +1,45 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import net.sourceforge.pmd.RuleSet; + +/** + * Wraps a ruleset, to provide a user-friendly string representation + */ +public class RuleSetWrapper { + + private final RuleSet ruleSet; + + public RuleSetWrapper(RuleSet ruleSet) { + this.ruleSet = ruleSet; + } + + public RuleSet getRuleSet() { + return ruleSet; + } + + @Override + public String toString() { + return String.format("%s (%s)", ruleSet.getName(), ruleSet.getFileName()); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanError.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanError.java new file mode 100644 index 0000000..85acd92 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanError.java @@ -0,0 +1,74 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.easypmd7.ThrowableExtensions; + +class ScanError implements ScanMessage { + + private static final int MAX_STACK_TRACE_STRING_LENGTH = 2000; + private static final String ELLIPSIS_STRING = "\n<...>"; + private final String stackTraceString; + private final String message; + + public ScanError(Exception exception) { + String fullStackTraceString = ThrowableExtensions.getStackTraceString(exception); + + if (fullStackTraceString.length() < MAX_STACK_TRACE_STRING_LENGTH) { + this.stackTraceString = fullStackTraceString; + } else { + this.stackTraceString = fullStackTraceString.substring(0, MAX_STACK_TRACE_STRING_LENGTH - ELLIPSIS_STRING.length() - 1) + ELLIPSIS_STRING; + } + + this.message = ThrowableExtensions.getNonEmptyMessage(exception); + } + + @Override + public String getAnnotationText() { + return String.format(stackTraceString); + } + + @Override + public int getLineNumber() { + return 1; + } + + @Override + public String getTaskText() { + return message; + } + + @Override + public String getTaskType() { + return "info.gianlucacosta.easypmd7.ide.tasklist.ScanError"; + } + + @Override + public String getAnnotationType() { + return "info.gianlucacosta.easypmd7.ide.annotations.ScanError"; + } + + @Override + public boolean isShowableInGuardedSections() { + return true; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessage.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessage.java new file mode 100644 index 0000000..832b107 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessage.java @@ -0,0 +1,42 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import java.io.Serializable; + +/** + * A message emitted during a PMD scan + */ +public interface ScanMessage extends Serializable { + + int getLineNumber(); + + String getTaskText(); + + String getTaskType(); + + String getAnnotationText(); + + String getAnnotationType(); + + boolean isShowableInGuardedSections(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessageList.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessageList.java new file mode 100644 index 0000000..6cee5d5 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanMessageList.java @@ -0,0 +1,45 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import java.util.ArrayList; +import java.util.Set; + +/** + * A list of scan messages + */ +public class ScanMessageList extends ArrayList { + + public ScanMessageList filterOutGuardedSections(Set guardedSectionLines) { + ScanMessageList result = new ScanMessageList(); + + for (ScanMessage scanMessage : this) { + int violationLineNumber = scanMessage.getLineNumber(); + + if (scanMessage.isShowableInGuardedSections() || !guardedSectionLines.contains(violationLineNumber)) { + result.add(scanMessage); + } + } + + return result; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanViolation.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanViolation.java new file mode 100644 index 0000000..3caae52 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/ScanViolation.java @@ -0,0 +1,153 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.Options; +import info.gianlucacosta.easypmd7.ide.options.OptionsService; +import net.sourceforge.pmd.RulePriority; +import net.sourceforge.pmd.RuleViolation; + +class ScanViolation implements ScanMessage { + + private static final String ANNOTATION_TOKEN_SEPARATOR = "\n"; + private static final String TASK_TOKEN_SEPARATOR = " "; + private final int lineNumber; + private final String description; + private final String ruleName; + private final String ruleSetName; + private final RulePriority priority; + + private transient String taskText; + private transient String annotationText; + + public ScanViolation(RuleViolation ruleViolation) { + lineNumber = ruleViolation.getBeginLine(); + description = ruleViolation.getDescription(); + ruleName = ruleViolation.getRule().getName(); + ruleSetName = ruleViolation.getRule().getRuleSetName(); + priority = ruleViolation.getRule().getPriority(); + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + private void verifyTransientFields() { + if (taskText != null && annotationText != null) { + return; + } + + OptionsService optionsService = Injector.lookup(OptionsService.class); + Options options = optionsService.getOptions(); + + taskText = formatViolationComponents(TASK_TOKEN_SEPARATOR, options.isShowRulePriorityInTasks(), options.isShowDescriptionInTasks(), options.isShowRuleInTasks(), options.isShowRuleSetInTasks()); + annotationText = formatViolationComponents(ANNOTATION_TOKEN_SEPARATOR, false, true, true, true); + } + + @Override + public String getTaskText() { + verifyTransientFields(); + + return taskText; + } + + @Override + public String getTaskType() { + switch (priority) { + case HIGH: + return "info.gianlucacosta.easypmd7.ide.tasklist.High"; + case MEDIUM_HIGH: + return "info.gianlucacosta.easypmd7.ide.tasklist.MediumHigh"; + case MEDIUM: + return "info.gianlucacosta.easypmd7.ide.tasklist.Medium"; + case MEDIUM_LOW: + return "info.gianlucacosta.easypmd7.ide.tasklist.MediumLow"; + case LOW: + return "info.gianlucacosta.easypmd7.ide.tasklist.Low"; + default: + throw new RuntimeException(String.format("Unexpected priority value: '%s'", priority)); + } + } + + @Override + public String getAnnotationText() { + verifyTransientFields(); + + return annotationText; + } + + @Override + public String getAnnotationType() { + switch (priority) { + case HIGH: + return "info.gianlucacosta.easypmd7.ide.annotations.High"; + case MEDIUM_HIGH: + return "info.gianlucacosta.easypmd7.ide.annotations.MediumHigh"; + case MEDIUM: + return "info.gianlucacosta.easypmd7.ide.annotations.Medium"; + case MEDIUM_LOW: + return "info.gianlucacosta.easypmd7.ide.annotations.MediumLow"; + case LOW: + return "info.gianlucacosta.easypmd7.ide.annotations.Low"; + default: + throw new RuntimeException(String.format("Unexpected priority value: '%s'", priority)); + } + } + + private String formatViolationComponents(String separator, boolean showPriority, boolean showDescription, boolean showRuleName, boolean showRuleSetName) { + StringBuilder resultBuilder = new StringBuilder(); + + if (showPriority) { + resultBuilder.append(priority.getPriority()); + resultBuilder.append(" - "); + } + + if (showDescription) { + resultBuilder.append(description); + } + + if (showRuleName) { + if (resultBuilder.length() > 0) { + resultBuilder.append(separator); + } + resultBuilder.append("Rule: "); + resultBuilder.append(ruleName); + } + + if (showRuleSetName) { + if (resultBuilder.length() > 0) { + resultBuilder.append(separator); + } + resultBuilder.append("Rule set: "); + resultBuilder.append(ruleSetName); + } + + return resultBuilder.toString(); + } + + @Override + public boolean isShowableInGuardedSections() { + return false; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/StandardRuleSetsCatalog.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/StandardRuleSetsCatalog.java new file mode 100644 index 0000000..389ec92 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/StandardRuleSetsCatalog.java @@ -0,0 +1,34 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner; + +import java.util.Collection; + +/** + * Catalog of standard PMD rulesets + */ +public interface StandardRuleSetsCatalog { + + boolean containsFileName(String ruleSetFileName); + + Collection getRuleSetWrappers(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/AbstractScanMessagesCache.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/AbstractScanMessagesCache.java new file mode 100644 index 0000000..715bab5 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/AbstractScanMessagesCache.java @@ -0,0 +1,95 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; + +import java.io.File; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Abstract implementation of the scan messages cache + */ +abstract class AbstractScanMessagesCache implements ScanMessagesCache { + + private final Lock readLock; + private final Lock writeLock; + + public AbstractScanMessagesCache() { + ReadWriteLock cacheLock = new ReentrantReadWriteLock(); + + readLock = cacheLock.readLock(); + writeLock = cacheLock.writeLock(); + } + + @Override + public ScanMessageList getScanMessagesFor(File file) { + readLock.lock(); + + try { + ScanMessagesCacheItem cacheItem = getItem(file); + + if (cacheItem == null) { + return null; + } + + if (!cacheItem.isSynchronizedWith(file)) { + return null; + } + + return cacheItem.getScanMessages(); + } finally { + readLock.unlock(); + } + } + + @Override + public void putScanMessagesFor(File file, ScanMessageList scanMessages) { + writeLock.lock(); + try { + ScanMessagesCacheItem cacheItem = new ScanMessagesCacheItem(file, scanMessages); + + putItem(file, cacheItem); + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean clear() { + writeLock.lock(); + + try { + return doClear(); + } finally { + writeLock.unlock(); + } + } + + protected abstract ScanMessagesCacheItem getItem(File scannedFile); + + protected abstract void putItem(File scannedFile, ScanMessagesCacheItem cacheItem); + + protected abstract boolean doClear(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/InMemoryScanMessagesCache.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/InMemoryScanMessagesCache.java new file mode 100644 index 0000000..ffb7e7a --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/InMemoryScanMessagesCache.java @@ -0,0 +1,51 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * In-memory scan messages cache + */ +class InMemoryScanMessagesCache extends AbstractScanMessagesCache { + + private final Map cacheItems = new HashMap<>(); + + @Override + protected ScanMessagesCacheItem getItem(File scannedFile) { + return cacheItems.get(scannedFile.getAbsolutePath()); + } + + @Override + protected void putItem(File scannedFile, ScanMessagesCacheItem cacheItem) { + cacheItems.put(scannedFile.getAbsolutePath(), cacheItem); + } + + @Override + protected boolean doClear() { + cacheItems.clear(); + + return true; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCache.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCache.java new file mode 100644 index 0000000..6ee2a92 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCache.java @@ -0,0 +1,38 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; + +import java.io.File; + +/** + * Cache of scan messages + */ +public interface ScanMessagesCache { + + ScanMessageList getScanMessagesFor(File file); + + void putScanMessagesFor(File file, ScanMessageList scanMessages); + + boolean clear(); +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheFacade.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheFacade.java new file mode 100644 index 0000000..1ebc610 --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheFacade.java @@ -0,0 +1,90 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import info.gianlucacosta.easypmd7.StorageAreaService; +import info.gianlucacosta.easypmd7.ide.Injector; +import info.gianlucacosta.easypmd7.ide.options.OptionsService; +import info.gianlucacosta.helios.beans.events.TriggerListener; +import org.openide.util.lookup.ServiceProvider; + +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The scan messages cache facade, providing the 2-tier cache + */ +@ServiceProvider(service = ScanMessagesCache.class) +public class ScanMessagesCacheFacade extends AbstractScanMessagesCache { + + private static final Logger logger = Logger.getLogger(AbstractScanMessagesCache.class.getName()); + private final OptionsService optionsService; + private final InMemoryScanMessagesCache inMemoryCache = new InMemoryScanMessagesCache(); + private final StorageAreaBasedScanMessagesCache onDiskCache; + + public ScanMessagesCacheFacade() { + optionsService = Injector.lookup(OptionsService.class); + + StorageAreaService storageAreaService = Injector.lookup(StorageAreaService.class); + + onDiskCache = new StorageAreaBasedScanMessagesCache(storageAreaService.getStorageArea()); + + optionsService.addOptionsChangedListener(new TriggerListener() { + @Override + public void onTriggered() { + logger.log(Level.INFO, "The options have changed: clearing the cache"); + clear(); + } + }); + } + + @Override + protected ScanMessagesCacheItem getItem(File file) { + ScanMessagesCacheItem memoryCacheItem = inMemoryCache.getItem(file); + + if (memoryCacheItem != null) { + return memoryCacheItem; + } + + ScanMessagesCacheItem storageCacheItem = onDiskCache.getItem(file); + + if (storageCacheItem != null) { + inMemoryCache.putItem(file, storageCacheItem); + + return storageCacheItem; + } + + return null; + } + + @Override + protected void putItem(File file, ScanMessagesCacheItem cacheItem) { + inMemoryCache.putItem(file, cacheItem); + onDiskCache.putItem(file, cacheItem); + } + + @Override + protected boolean doClear() { + return inMemoryCache.clear() && onDiskCache.clear(); + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheItem.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheItem.java new file mode 100644 index 0000000..633c50c --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/ScanMessagesCacheItem.java @@ -0,0 +1,53 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import info.gianlucacosta.easypmd7.pmdscanner.ScanMessageList; + +import java.io.File; +import java.io.Serializable; + +/** + * An item in the scan messages cache + */ +public class ScanMessagesCacheItem implements Serializable { + + private final long lastModified; + private final ScanMessageList scanMessages; + + ScanMessagesCacheItem(File file, ScanMessageList scanMessages) { + if (scanMessages == null) { + throw new IllegalArgumentException(); + } + + this.lastModified = file.lastModified(); + this.scanMessages = scanMessages; + } + + public boolean isSynchronizedWith(File file) { + return lastModified == file.lastModified(); + } + + public ScanMessageList getScanMessages() { + return scanMessages; + } +} diff --git a/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/StorageAreaBasedScanMessagesCache.java b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/StorageAreaBasedScanMessagesCache.java new file mode 100644 index 0000000..35a466b --- /dev/null +++ b/src/main/java/info/gianlucacosta/easypmd7/pmdscanner/messagescache/StorageAreaBasedScanMessagesCache.java @@ -0,0 +1,143 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +package info.gianlucacosta.easypmd7.pmdscanner.messagescache; + +import info.gianlucacosta.easypmd7.io.StreamUtils; +import info.gianlucacosta.helios.io.storagearea.StorageArea; +import info.gianlucacosta.helios.io.storagearea.StorageAreaEntry; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Scan messages cache writing its items to a StorageArea + */ +class StorageAreaBasedScanMessagesCache extends AbstractScanMessagesCache { + + private static final Logger logger = Logger.getLogger(StorageAreaBasedScanMessagesCache.class.getName()); + private static final String ROOT_CACHE_ENTRY = "cache"; + private final StorageArea storageArea; + + public StorageAreaBasedScanMessagesCache(StorageArea storageArea) { + this.storageArea = storageArea; + } + + @Override + protected ScanMessagesCacheItem getItem(File scannedFile) { + if (storageArea == null) { + logger.log(Level.FINE, "No storage area detected - reading cannot be performed"); + return null; + } + + Map clusterMap = getClusterMap(scannedFile); + + if (clusterMap == null) { + return null; + } + + return clusterMap.get(scannedFile.getAbsolutePath()); + } + + private Map getClusterMap(File scannedFile) { + try { + StorageAreaEntry clusterMapEntry = getClusterMapEntry(scannedFile); + + if (!clusterMapEntry.exists()) { + return null; + } + + try (InputStream entryStream = clusterMapEntry.openInputStream()) { + return (Map) StreamUtils.readSingleObjectFromStream(entryStream); + } + + } catch (IOException | ClassNotFoundException ex) { + logger.log( + Level.WARNING, + String.format( + "Error while reading cache cluster map for file: '%s'", + scannedFile.getAbsolutePath()), + ex); + + } + + return null; + } + + private StorageAreaEntry getClusterMapEntry(File scannedFile) throws IOException { + String absolutePath = scannedFile.getAbsolutePath(); + + Integer pathHashCode = absolutePath.hashCode(); + return storageArea.getEntry(ROOT_CACHE_ENTRY, pathHashCode.toString()); + } + + @Override + protected void putItem(File scannedFile, ScanMessagesCacheItem cacheItem) { + if (storageArea == null) { + logger.log(Level.FINE, "No storage area detected - bypassing writing phase..."); + return; + } + + Map clusterMap = getClusterMap(scannedFile); + + if (clusterMap == null) { + clusterMap = new HashMap<>(); + } + + clusterMap.put(scannedFile.getAbsolutePath(), cacheItem); + + try { + StorageAreaEntry clusterMapEntry = getClusterMapEntry(scannedFile); + + try (OutputStream entryOutputStream = clusterMapEntry.openOutputStream()) { + StreamUtils.writeSingleObjectToStream(entryOutputStream, clusterMap); + } + } catch (IOException ex) { + logger.log( + Level.SEVERE, + String.format("Error while writing the cache cluster map for file: '%s'", scannedFile.getAbsolutePath()), + ex); + } + } + + @Override + protected boolean doClear() { + if (storageArea == null) { + logger.log(Level.FINE, "No storage area detected - the cache is already empty"); + return true; + } + + try { + StorageAreaEntry rootCacheEntry = storageArea.getEntry(ROOT_CACHE_ENTRY); + rootCacheEntry.remove(); + return true; + } catch (IOException ex) { + logger.log(Level.SEVERE, "Could not clear the cache", ex); + return false; + } + } +} diff --git a/src/main/nbm/manifest.mf b/src/main/nbm/manifest.mf new file mode 100644 index 0000000..c669f3e --- /dev/null +++ b/src/main/nbm/manifest.mf @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +OpenIDE-Module-Localizing-Bundle: info/gianlucacosta/easypmd7/Bundle.properties +OpenIDE-Module-Layer: info/gianlucacosta/easypmd7/layer.xml +OpenIDE-Module-Java-Dependencies: Java > 1.8 \ No newline at end of file diff --git a/src/main/resources/info/gianlucacosta/easypmd7/Bundle.properties b/src/main/resources/info/gianlucacosta/easypmd7/Bundle.properties new file mode 100644 index 0000000..6d13c28 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/Bundle.properties @@ -0,0 +1,26 @@ +### +# ==========================================================================%%# +# EasyPmd +# ===========================================================================%% +# Copyright (C) 2009 - 2015 Gianluca Costa +# ===========================================================================%% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program. If not, see +# . +# ==========================================================================%## +### +OpenIDE-Module-Display-Category=Java +OpenIDE-Module-Name=EasyPmd +OpenIDE-Module-Short-Description=Performs code analysis by using PMD and shows the results both in the editor and in the Action Items window +OpenIDE-Module-Long-Description=

Performs code analysis by using PMD and shows the results both in the editor and in the Action Items window.

For further information, please refer to the plugin's home page.

+ diff --git a/src/main/resources/info/gianlucacosta/easypmd7/Plugin.properties.xml b/src/main/resources/info/gianlucacosta/easypmd7/Plugin.properties.xml new file mode 100644 index 0000000..a94d8ef --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/Plugin.properties.xml @@ -0,0 +1,30 @@ + + + + + ${project.name} + ${project.version} + ${project.url} + ${facebookPage} + ${isRelease} + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/about.html b/src/main/resources/info/gianlucacosta/easypmd7/docs/about.html new file mode 100644 index 0000000..ef2907c --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/about.html @@ -0,0 +1,60 @@ + + + + + + + EasyPmd - About + + + + + +

EasyPmd

+ +
+

Scan your code with PMD inside NetBeans

+ +

+ Elegance always matters, especially when creating software, + and EasyPmd has been designed to simplify the application of PMD to + your source code. +

+ +

+ For further information, please visit the plugin's + home page +

+
+ + +
+

Legal notice

+ +
+ This product includes software developed in part by support from the Defense Advanced Research Project Agency + (DARPA). +
+
+ + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-hs.xml b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-hs.xml new file mode 100644 index 0000000..b6cad45 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-hs.xml @@ -0,0 +1,51 @@ + + + + + + + EasyPmd Help + + info.gianlucacosta.easypmd7.about + + + + TOC + + javax.help.TOCView + easypmd-toc.xml + + + Index + + javax.help.IndexView + easypmd-idx.xml + + + Search + + javax.help.SearchView + JavaHelpSearch + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-idx.xml b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-idx.xml new file mode 100644 index 0000000..8235c05 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-idx.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-map.xml b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-map.xml new file mode 100644 index 0000000..e20f719 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-map.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-toc.xml b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-toc.xml new file mode 100644 index 0000000..39599ba --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd-toc.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd.css b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd.css new file mode 100644 index 0000000..a1c4bd7 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/easypmd.css @@ -0,0 +1,36 @@ +/* + * ==========================================================================%%# + * EasyPmd + * ===========================================================================%% + * Copyright (C) 2009 - 2015 Gianluca Costa + * ===========================================================================%% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * ==========================================================================%## + */ +li { + margin-top: 10px; + margin-bottom: 10px; +} + + +section, div { + margin-top: 10px; + margin-bottom: 10px; +} + +.options > section { + margin-top: 20px; + margin-bottom: 20px; +} diff --git a/src/main/resources/info/gianlucacosta/easypmd7/docs/options.html b/src/main/resources/info/gianlucacosta/easypmd7/docs/options.html new file mode 100644 index 0000000..ad0e038 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/docs/options.html @@ -0,0 +1,347 @@ + + + + + + + EasyPmd - Options + + + + + + +
+

EasyPmd options

+ + +
+

Profiles

+ +

+ EasyPmd includes Profiles: + a profile is simply a named configuration for EasyPmd - + you can therefore change the values of any set of options + simply by selecting another profile. +

+ +

+ EasyPmd comes with a default profile: by duplicating it, + you can create new profiles, having different values for + any options, as needed. +

+ +

+ Using profiles is quite straightforward, but a few notes + are due: +

+ +
    +
  • + When editing EasyPmd's options, they are temporarily but + constantly saved into the active profile - the one selected + in the Profile drop-down list: if you select another + profile, its options are immediately applied to all the + controls in the dialog and you can start editing the new + profile. If you re-select the previous profile, you + will be able to edit its options again. +
  • + +
  • + Changes to the profiles, including the active profile, + are saved to disk only if you confirm the options dialog + by pressing OK; this will also apply the new set of + options to the internal PMD scanner. + Consequently, if you cancel the dialog, no change will + be saved - not even into memory. +
  • + +
  • + The initial profile can be deleted, if you wish: the + only constraint is the fact that at least one profile + must be present in the system - EasyPmd will just prevent + you from removing the last profile. +
  • +
+
+ +
+

General options

+ +

This group of options determines how PMD should read your source files, in particular what the code structure + is (determined by the Java version), which encoding should be used and what lines should + be ignored.

+ +
    +
  • + Target Java version: this parameter is used by PMD's parser in order to determine the structure + of source files. At the moment of this writing, the latest Java version is "1.7". +
  • + +
  • + Source file encoding: the encoding used to read source files when scanning them with PMD. +
  • + +
  • + Suppress marker: the marker comment used to prevent PMD checks for a specific line. Please + refer to PMD's manual. +
  • + +
  • + Minimum priority: PMD will output violations only for rules having at least the selected + priority. +
  • +
+
+ + +
+

Additional classpath options

+ +

+ The additional classpath tells PMD where to look for additional, that is custom, rulesets. + If you just want to use the standard rulesets provided by PMD, you can leave this list empty. +

+ +
    +
  • Add Jar...: shows a file dialog to let the user choose a jar file, so as to add it to PMD's + classpath. +
  • +
  • Add custom URL...: allows the user to add a custom URL to PMD's classpath.
  • +
  • Move up: moves the selected URLs up.
  • +
  • Move down: moves the selected URLs down.
  • +
  • Remove: removes the selected URLs from the list.
  • +
+
+ + +
+

Rulesets options

+ +

+ A ruleset defines a set of rules which have to be considered by PMD during a scan. +

+ +

+ You can use PMD's standard rulesets, or you can define your own ones, as well as your own rules; for further + information on creating your custom rulesets, + please refer to the related article on my website. +

+ +
    +
  • Add standard...: shows a dialog reporting PMD's default rulesets. The user can choose one of + them in order to add it to the list. +
  • +
  • Add custom...: allows the user to add a custom ruleset to the list.
  • +
  • Move up: moves the selected rulesets up.
  • +
  • Move down: moves the selected rulesets down.
  • +
  • Remove: removes the selected rulesets from the list.
  • +
+
+ + +
+

Cache options

+ +

+ EasyPmd can provide an internal cache, which stores all the scan results; as long as your source files are + unchanged, EasyPmd will look for the cache content + instead of executing PMD again. + EasyPmd's cache is persisted on disk, which means that it will be used even after you restart NetBeans. +

+ +

+ In addition to this, it's important to notice that the cache is not entirely reloaded in memory + when you start NetBeans: EasyPmd loads cache items in memory only + when they are required by the scanning engine. + For example, if you set the scanning context - in the Action Items window of NetBeans - to show + tasks only for the current file, the scan results will be loaded + from the on-disk cache into the in-memory cache only for the files that you open. +

+ +

+ WARNING: please, remember that the cache is entirely cleared whenever you change any + option and confirm the Options dialog. +

+ +
    +
  • Activate EasyPmd cache: when this checkbox is checked, EasyPmd will store the scan output into + its cache, which resides both in memory and on disk. +
  • +
  • Clear the cache: removes every cache entry, both in memory and on disk. + This can be very useful because EasyPmd stores its cache on disk too, in the directory HOME_DIR/.EasyPmd{major + version}/cache. Should this directory grow too big, you can delete it, either manually or by + clicking this button (which also empties the in-memory cache). +
  • +
+
+ + +
+

Reporting options

+ +

+ These options simply affect how EasyPmd reports the results of the scans performed by PMD. +

+ +
    +
  • + Show rule priority in tasks: when it's active, you can easily sort violations in the Action + Items window just by clicking the Description column header. +
  • + +
  • + Show violation description in tasks: shows, in the Action Items window, the + description associated with each violation. +
  • + + +
  • + Show rule in tasks: shows, in the Action Items window, which rule caused each + violation. +
  • + + +
  • + Show ruleset in tasks: shows, in the Action Items window, which ruleset caused each + violation. +
  • + + +
  • + Show annotations in the code editor: if checked, it will cause the editor + to display a glyph next to each line associated with an EasyPmd message. + +

    NOTE: the process of creating annotations appears to take about 60% of EasyPmd's + scan time. Therefore, you might want to consider disabling this option if you are concerned about + scan speed.

    +
  • + + +
  • + Show all messages in guarded sections: if unchecked, EasyPmd will NOT show annotations or tasks + for violations in lines protected by a guarded section. +
  • +
+
+ + +
+

Path filtering

+ +
+ EasyPmd supports path filtering, which means that you can make EasyPmd scan only some files in your + project, and you can at the same time exclude other files. + + There are some important concepts that should be kept in mind when setting path filters for EasyPmd: + +
    +
  1. You can choose which paths have to be filtered (that is, included or excluded) by inserting + one or more regular expressions
  2. + +
  3. Regular expressions are, by default, case sensitive, therefore on all operating + systems, including Windows, a regular expression will match a file only if the path components have + the correct case. + Of course, you can create a case-insensitive regular expression, either manually or by + using the related feature provided by EasyPmd +
  4. + +
  5. To decide whether a file should be scanned, its absolute path is considered and + matched against your set of regular expressions +
  6. + +
  7. Always use "/" to separate path components: it will be automatically replaced by + the regex-compatible form of the separator used by your system, if possible (in particular, it + should work on Windows, Linux and Unix-like systems) +
  8. + +
  9. If you need some examples, consider adding some predefined + regular expressions
  10. +
+
+ + +
+

Scanning algorithm

+ + EasyPmd observes the following rules when determining if PMD should scan a file: + +
    +
  1. If the absolute path matches at least one of the regular expressions in the list + of included patterns, or if this list is empty, consider the next step, otherwise the file + is not scanned +
  2. +
  3. Then, if the absolute path does not match any pattern in the excluded + patterns list, or if this list is empty, scan the file +
  4. +
+
+ + +
+

Predefined regular expressions

+ +

+ Regular expressions are not always easy to write, and they might easily become tedious and error-prone + if you have to write them frequently to cope with common cases - for example, filtering by file + extension. +

+ +

+ Therefore, EasyPmd provides some predefined regular expressions, which will be automatically + generated by EasyPmd according to some information you will be asked for (e.g: the file extension). + Predefined regular expressions are available for both included and excluded paths, and can be added by + clicking on one of the Add predefined... buttons in the Path filtering panel. +

+
+
+ + +
+

Miscellaneous

+ +
    +
  • + Auxiliary classpath: the auxiliary classpath employed by PMD. Please refer to PMD's + documentation +
  • +
+
+ +
+

Commands

+
    +
  • + Reset settings: restores all the settings in the dialog to their original values, but you have + to confirm the dialog in order to save them. +
  • + +
  • + Verify settings: performs a first-level check on the options in the dialog, telling if they + seem correct +
  • +
+
+
+ + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/Bundle.properties b/src/main/resources/info/gianlucacosta/easypmd7/ide/Bundle.properties new file mode 100644 index 0000000..b598702 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/Bundle.properties @@ -0,0 +1,23 @@ +### +# ==========================================================================%%# +# EasyPmd +# ===========================================================================%% +# Copyright (C) 2009 - 2015 Gianluca Costa +# ===========================================================================%% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program. If not, see +# . +# ==========================================================================%## +### +Filter_DisplayName=EasyPmd notifications +Filter_Description=EasyPmd diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/highDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/highDescriptor.xml new file mode 100644 index 0000000..6295b4b --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/highDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/lowDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/lowDescriptor.xml new file mode 100644 index 0000000..93c822f --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/lowDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumDescriptor.xml new file mode 100644 index 0000000..7ce0d4e --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumHighDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumHighDescriptor.xml new file mode 100644 index 0000000..eb0062e --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumHighDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumLowDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumLowDescriptor.xml new file mode 100644 index 0000000..bf26aaf --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/mediumLowDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/scanErrorDescriptor.xml b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/scanErrorDescriptor.xml new file mode 100644 index 0000000..f8f69fe --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/annotations/scanErrorDescriptor.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/high.png b/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/high.png new file mode 100644 index 0000000000000000000000000000000000000000..c02cb0f3bb4d44c2e5cea298e67065de40008b73 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkEqds;&0mS^xnktgrVSF&IwM3rridT+s`H? z82(h8!Wvh0`s&i_p{umsNj^T8Ih{{6c1vjJuTX~|*)nUb$*Y`LcO<^^SQR|6Y}cPp z8&r=?>{_tNLBaftbo=q+#uK+i+{^3y=wa^{;uG?W=ajUiN`Oq+?$#5B9Qzge-G5nJ zTHg6e)^mkvlxnQP=GQ!0O>&O&)qXwq=ywp>7o}*t#&p$l-o{muacYvnhJ7;bE?m2M^qC*dFOq*XkJR8T9sg_6a88X|jq> zc%|8LGHmwl(`8zES@8ABxns;*Ep#+7G QH$b81>FVdQ&MBb@09sv@wEzGB literal 0 HcmV?d00001 diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/low.png b/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/low.png new file mode 100644 index 0000000000000000000000000000000000000000..539743a06d352e846217f470a8bf7698ea7a7efd GIT binary patch literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=DinK$vl=HlH+5kiEpy*OmPqhY+KMX(XG1Nt5+xqYB6RhU%SuHlp+Vs3q6cysElLU{B6D~3Om)f-b`cZtk(=T)MPXIP zrbP#~sr>x)l<#Ikfu?#u)@S*y<1Etzg_NhOpUXO@geCwz!;7B) literal 0 HcmV?d00001 diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumHigh.png b/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumHigh.png new file mode 100644 index 0000000000000000000000000000000000000000..04616687ed27fbc3adac26e1573eff41c9209734 GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE5|$piSL=Ieggy;t@VvV1>+Iay`8Ny78g2v^TxNJIVH;#8@OGE*5w(M* zylb=pQ24?jl3E4LS!57%o4_4NaJrUy?Ylc3UcE8PEQ!$6~ck8GT)! z&2^^C@ksopYpXT5-YYXccXHHcJlCpojAcRb^yVWKQ~Zquv~P)SS~aQh(MSCWA#bgo lWEJzAW2%!){rA^=A49iMnOD)*z-EwtJzf1=);T3K0RRi1apV91 literal 0 HcmV?d00001 diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumLow.png b/src/main/resources/info/gianlucacosta/easypmd7/ide/icons/mediumLow.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9df7e96afb50f30a2e7630dfdde314174676ff GIT binary patch literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=DinK$vl=HlH+5kiEpy*OmPqhY+KgQnB669-z<W&e^db z+qR)XxN!aRk~`A=N^%a5$_|9S+WA5*`FKzBnL|x+CFWnRo?ksFw{PPEkJs1bRM##` zjb>+@^ykOFp4=OWCjWI6f8AsDyw}6L#8bM6O`}ESonGE7pI>I59Q+=fv=CNrn8>2| z;z*hdP;8sqs^mu}Cq%BAu)$SeLxw;1iJ55eg zG`#M_-!n7H+R4polB+oW7`!m((=WDjT$9oIpY_`o>bY7x% zyTYUmTmjcs^h?nz}sf`M7Zo!xuRy=G+C!m(j`b6sQSL6fF%_Nd>_1DDIJ zzd5hiLhOU`2^MzKlcDjeE;B|jo%S>}+rgw{x~T3`C;v6WGe%qH99URTqsSq+hjjys z=F()2$t^QCdOy%}eSKqpy@<_J-x!u1X6&{p%)V>XHhAy(?S4=vTPn;qJ;v{m9joWH z?~TrR(Zzn&rPtM@OOm9E63hM8*ht-)&CQ)!%U*tD`nHdAp7Do?A5;z#n)U@4nhc(< KelF{r5}E*pKcce$ literal 0 HcmV?d00001 diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/options/Bundle.properties b/src/main/resources/info/gianlucacosta/easypmd7/ide/options/Bundle.properties new file mode 100644 index 0000000..1dc7efd --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/options/Bundle.properties @@ -0,0 +1,73 @@ +### +# ==========================================================================%%# +# EasyPmd +# ===========================================================================%% +# Copyright (C) 2009 - 2015 Gianluca Costa +# ===========================================================================%% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program. If not, see +# . +# ==========================================================================%## +### +Option_DisplayName_EasyPmd=EasyPmd +Option_Keywords_EasyPmd=EasyPmd PMD +Option_KeywordsCategory_EasyPmd=EasyPmd,PMD +EasyPmdPanel.clearCacheButton.text=Clear cache +EasyPmdPanel.sourceFileEncodingLabel.text=Source file encoding: +EasyPmdPanel.sourceFileEncodingField.text= +EasyPmdPanel.suppressMarkerLabel.text=Suppress marker: +EasyPmdPanel.verifySettingsButton.text=Verify settings +EasyPmdPanel.showAnnotationsInEditorCheckBox.text=Show annotations in the code editor +EasyPmdPanel.showRuleInTasksCheckBox.text=Show rule name in tasks +EasyPmdPanel.showRuleSetInTasksCheckBox.text=Show ruleset name in tasks +EasyPmdPanel.useScanMessagesCacheCheckBox.text=Activate EasyPmd cache +EasyPmdPanel.clearScanMessagesCacheButton.label= +EasyPmdPanel.clearScanMessagesCacheButton.text=Clear the cache +EasyPmdPanel.resetSettingsButton.text=Reset settings +EasyPmdPanel.generalPanel.TabConstraints.tabTitle=General +EasyPmdPanel.additionalClasspathPanelContainer.TabConstraints.tabTitle=Ruleset classpath +EasyPmdPanel.mainRuleSetsPanel.TabConstraints.tabTitle=Rulesets +EasyPmdPanel.cachePanel.TabConstraints.tabTitle=Cache +EasyPmdPanel.reportingPanel.TabConstraints.tabTitle=Reporting +EasyPmdPanel.mainAdditionalClasspathPanel.TabConstraints.tabTitle=Additional classpath +EasyPmdPanel.showDescriptionInTasksCheckBox.text=Show violation description in tasks +EasyPmdPanel.showAllMessagesInGuardedSectionsCheckBox.text=Show all messages in guarded sections +EasyPmdPanel.mainPathFilteringPanel.TabConstraints.tabTitle=Path filtering +EasyPmdPanel.profileLabel.text=Profile: +EasyPmdPanel.renameProfileButton.text=Rename... +EasyPmdPanel.removeProfileButton.text=Remove... +EasyPmdPanel.duplicateProfileButton.text=Duplicate... +PathFilteringPanel.excludedPathsPanelContainer.border.title=Excluded file patterns +PathFilteringPanel.includedPathsPanelContainer.border.title=Included file patterns +AdditionalClasspathPanel.addJarButton.text=Add Jar... +AdditionalClasspathPanel.addCustomUrlButton.text=Add custom URL... +AdditionalClasspathPanel.moveUpButton.text=Move up +AdditionalClasspathPanel.moveDownButton.text=Move down +AdditionalClasspathPanel.removeButton.text=Remove +RuleSetsPanel.addStandardRuleSetButton.text=Add standard... +RuleSetsPanel.addCustomRuleSetButton.text=Add custom... +RuleSetsPanel.moveUpButton.text=Move up +RuleSetsPanel.moveDownButton.text=Move down +RuleSetsPanel.removeButton.text=Remove +EasyPmdPanel.minimumPriorityLabel.text=Minimum priority: +EasyPmdPanel.showRulePriorityInTasksCheckBox.text=Show rule priority in tasks +EasyPmdPanel.showFacebookPageButton.text=Show Facebook page +EasyPmdPanel.showHomePageButton.text=Show home page +EasyPmdPanel.pluginIconPicture.text=Icon +EasyPmdPanel.pluginTitleLabel.text=Title +EasyPmdPanel.pmdVersionLabel.text=PMD version +EasyPmdPanel.targetJavaVersionLabel.text=Target Java version: +EasyPmdPanel.infoPanel.TabConstraints.tabTitle=About +EasyPmdPanel.auxiliaryClassPathLabel.text=Auxiliary classpath: +EasyPmdPanel.auxiliaryPathField.text= +EasyPmdPanel.miscPanel.TabConstraints.tabTitle=Miscellaneous diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/options/regexes/Bundle.properties b/src/main/resources/info/gianlucacosta/easypmd7/ide/options/regexes/Bundle.properties new file mode 100644 index 0000000..047ba50 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/options/regexes/Bundle.properties @@ -0,0 +1,27 @@ +### +# ==========================================================================%%# +# EasyPmd +# ===========================================================================%% +# Copyright (C) 2009 - 2015 Gianluca Costa +# ===========================================================================%% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program. If not, see +# . +# ==========================================================================%## +### + +RegexesPanel.addPredefinedPathButton.text=Add predefined... +RegexesPanel.addCustomPathButton.text=Add custom... +RegexesPanel.moveUpPathButton.text=Move up +RegexesPanel.moveDownPathButton.text=Move down +RegexesPanel.removePathButton.text=Remove diff --git a/src/main/resources/info/gianlucacosta/easypmd7/ide/tasklist/Bundle.properties b/src/main/resources/info/gianlucacosta/easypmd7/ide/tasklist/Bundle.properties new file mode 100644 index 0000000..68f3689 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/ide/tasklist/Bundle.properties @@ -0,0 +1,45 @@ +### +# ==========================================================================%%# +# EasyPmd +# ===========================================================================%% +# Copyright (C) 2009 - 2015 Gianluca Costa +# ===========================================================================%% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program. If not, see +# . +# ==========================================================================%## +### +HighTaskGroupDisplay=EasyPmd - High +HighTaskGroupDescription=Behavior is critically broken/buggy +HighIcon=info/gianlucacosta/easypmd7/ide/icons/high.png + +MediumHighTaskGroupDisplay=EasyPmd - Medium High +MediumHighTaskGroupDescription=Behavior is quite likely to be broken/buggy +MediumHighIcon=info/gianlucacosta/easypmd7/ide/icons/mediumHigh.png + +MediumTaskGroupDisplay=EasyPmd - Medium +MediumTaskGroupDescription=Behavior is confusing, perhaps buggy, and/or against standards/best practices +MediumIcon=info/gianlucacosta/easypmd7/ide/icons/medium.png + +MediumLowTaskGroupDisplay=EasyPmd - Medium Low +MediumLowTaskGroupDescription=Behavior is not likely to be buggy, but more just flies in the face of standards/style/good taste +MediumLowIcon=info/gianlucacosta/easypmd7/ide/icons/mediumLow.png + +LowTaskGroupDisplay=EasyPmd - Low +LowTaskGroupDescription=Nice to have, such as a consistent naming policy for package/class/fields... +LowIcon=info/gianlucacosta/easypmd7/ide/icons/low.png + + +ScanErrorTaskGroupDisplay=EasyPmd - Scan error +ScanErrorTaskGroupDescription=Parsing errors encountered while scanning a file +ScanErrorIcon=info/gianlucacosta/easypmd7/ide/icons/scanError.png diff --git a/src/main/resources/info/gianlucacosta/easypmd7/layer.xml b/src/main/resources/info/gianlucacosta/easypmd7/layer.xml new file mode 100644 index 0000000..ef72f98 --- /dev/null +++ b/src/main/resources/info/gianlucacosta/easypmd7/layer.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/info/gianlucacosta/easypmd7/mainIcon128.png b/src/main/resources/info/gianlucacosta/easypmd7/mainIcon128.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5e8f554cdc121b23750ecea520447095f5b254 GIT binary patch literal 7153 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 TO>^$UhXGJgREL$nFc0}ZF66Lg literal 0 HcmV?d00001 diff --git a/src/main/resources/info/gianlucacosta/easypmd7/mainIcon32.png b/src/main/resources/info/gianlucacosta/easypmd7/mainIcon32.png new file mode 100644 index 0000000000000000000000000000000000000000..9bf326654166548ec422022a346e7c5d28eed4df GIT binary patch literal 1591 zcmV-72FUq|P)k|CfDMFc8Tk(LT=q0mzL za&K?%?LGUT?MhWZzpxKJpL+i1dHJ1ln&+I0Fvc*FCK}P|PXPW80OaEYA%r9;VyZJ}s$ah9WyN}FF9v{GFnX7Vo1UkNm8~Ll3=4Ll8GV+yPPClkpTSH+qNx}%jG(U z!*MkTpvlf!y>U%$4)`22@clISoHY3QJiztS;OoDuaSj^1_vX3&Uf}w@dr#1@Iy3x$ z5E4DT`%A@8J4cWt{`AnHmd`hBl1ru1B!*!OB7_i9CnQXHoTF&a3%wX)1^`r4oJv02 zxVzFkrolF5(nnArKKJTLG)1q_VaEh z=Q5ex9-D21445Eb^zo5iRr4MGZvX&^Qkg_XMF(4| zkM?v`A3V4@G)AFTPdIt_@bhox=i2}PQYK9@B7_o20D!(eM>_x{hZiz*+C-<$)tW(| zeSV(dc`wcP*j=7lovp1#>#o2U`vCw%QYjT0H#U1_s!XXIPuuNx0$QKhY@D@tu?X_> z0RZ5{ks~IRN|i|h0MuQ(UbA8K()r;4-~aN@&CiO@Z5i&)1WYWEP?%xv>oEe$a)IB4 zg@rNzxa)N4x3kCU>b36hfLMhrDm!!hj7J6NZEm(Q4AUN}$>nl|Iwd7CFg{sco&o^v zA^-r3#nN-5rS(d9qW9K5H|F@hl^+)E$T_ef_oV_J zpO?38)2?0mfYxm?DKW;5U?*(Uwp@f)o}U?>EK#lSZ(fz904{lYgVs2$U}d81Xtiy> z#p3D-ItggNot`*vo(Magdovd;s*#Arve$O(kj|Jj%NUsdskm6_@py~?A$LJp`R^5- z7OODW%BD<=VfMX~26NJ=Wudyj(pxY~Ks(0%<)7j6{ zG#jd2{-)71`D{5}B9jHwwDlg2*qnH)h8!rLz;dyajSIePb+_^pelMdl-f zC;$MAG2>^L;10B3sN@V)Rb>y#GbliMTH-jVG-Aki`nrX&&Qm+bjnbqOD*~Fp^TE~H z+io`vXZ801WM^e840qsp0flP-01*@tl48YCficDc4FJG-Xi;}lQ`K;a1BCnuSLQBR zzW<9Yn#$_yPJMfu(bQ>fz1eCswA{9I5+oK&2|+BAAW9ueD3lVvJW}k7ztzRn1pyew z=l8M5R9R{5tf@IaTmTgKGFmWi&Ko4@XU_k1sUq~z_|QOv5CDh*S90{EXEb6$h`~H> pw(E3_j|?zUHsap}PXI<7;9rFOuI6u>gJS>y002ovPDHLkV1k<0@Tvd+ literal 0 HcmV?d00001