├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── nl │ │ └── futureedge │ │ └── sonar │ │ └── plugin │ │ └── issueresolver │ │ ├── IssueResolverPlugin.java │ │ ├── helper │ │ ├── IssueHelper.java │ │ ├── SearchHelper.java │ │ ├── TransitionHelper.java │ │ └── package-info.java │ │ ├── issues │ │ ├── IssueData.java │ │ ├── IssueKey.java │ │ └── package-info.java │ │ ├── json │ │ ├── JsonReader.java │ │ └── package-info.java │ │ ├── package-info.java │ │ ├── page │ │ ├── IssueResolverPage.java │ │ └── package-info.java │ │ └── ws │ │ ├── ExportAction.java │ │ ├── ImportAction.java │ │ ├── ImportResult.java │ │ ├── IssueResolverWebService.java │ │ ├── IssueResolverWsAction.java │ │ ├── UpdateAction.java │ │ └── package-info.java └── resources │ ├── response-examples │ ├── export.json │ └── import.json │ └── static │ ├── config.js │ ├── dom.js │ ├── entrypoint.js │ ├── main.js │ ├── require.js │ ├── result.js │ ├── tabExport.js │ ├── tabImport.js │ ├── tabUpdate.js │ └── tabsFactory.js └── test ├── it ├── pom.xml └── src │ └── main │ └── java │ └── TestClass.java ├── java └── nl │ └── futureedge │ └── sonar │ └── plugin │ └── issueresolver │ ├── IssueResolverPluginTest.java │ ├── PluginIT.java │ ├── issues │ ├── IssueDataTest.java │ ├── IssueKeyTest.java │ └── ReflectionTestUtils.java │ ├── json │ └── JsonReaderTest.java │ ├── page │ └── IssueResolverPageTest.java │ └── ws │ ├── ExportActionTest.java │ ├── ImportActionTest.java │ ├── IssueResolverWebServiceTest.java │ ├── MockRequest.java │ ├── MockResponse.java │ └── UpdateActionTest.java └── resources ├── logback.xml └── nl └── futureedge └── sonar └── plugin └── issueresolver └── ws └── ImportActionTest-request.json /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # GitHub template: java.gitignore 3 | # 4 | .class 5 | *.ctxt 6 | .mtj.tmp/ 7 | *.jar 8 | *.war 9 | *.ear 10 | hs_err_pid* 11 | 12 | # 13 | # GitHub template: java.gitignore 14 | # 15 | target/ 16 | pom.xml.tag 17 | pom.xml.releaseBackup 18 | pom.xml.versionsBackup 19 | pom.xml.next 20 | release.properties 21 | dependency-reduced-pom.xml 22 | buildNumber.properties 23 | .mvn/timing.properties 24 | !/.mvn/wrapper/maven-wrapper.jar 25 | 26 | # 27 | # eclipse 28 | # 29 | **/.project 30 | **/.settings 31 | **/.classpath 32 | **/.checkstyle 33 | **/.eclipse-pmd 34 | 35 | # 36 | build.cmd 37 | cp.cmd 38 | go.cmd 39 | startup.cmd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | install: true 4 | 5 | cache: 6 | directories: 7 | - '$HOME/.m2/repository' 8 | - '$HOME/.sonar/cache' 9 | - '$HOME/.sonar/installs' 10 | 11 | language: java 12 | 13 | jdk: 14 | - oraclejdk8 15 | 16 | addons: 17 | sonarqube: true 18 | 19 | script: 20 | - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent verify sonar:sonar -Dsonar.host.url=https://sonarqube.com -Dsonar.login=$SONAR_TOKEN -Dsonar.organization=$SONAR_ORGANIZATION 21 | 22 | deploy: 23 | provider: releases 24 | api_key: $GITHUB_OAUTH_TOKEN 25 | file_glob: true 26 | file: 'target/sonar-issueresolver-plugin-*.jar' 27 | skip_cleanup: true 28 | on: 29 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Issue resolver Plugin for SonarQube [![Build Status](https://travis-ci.org/willemsrb/sonar-issueresolver-plugin.svg?branch=master)](https://travis-ci.org/willemsrb/sonar-issueresolver-plugin) [![Quality Gate](https://sonarqube.com/api/badges/gate?key=nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin)](https://sonarqube.com/dashboard/index?id=nl.future-edge.sonarqube.plugins%3Asonar-issueresolver-plugin) 2 | *Requires SonarQube 6.3+* 3 | 4 | This plugin allows you to synchronize and export issue data (status, resolution, assignee and comments) of issues that have been confirmed, reopened or resolved. After exporting the data you can import it into a project where the issues in that project will be matched with the exported issue data; if matched the issue will be confirmed, reopened or resolved. Optionally the matched issue can be assigned to the same user and missing comments can be added. 5 | When working within one SonarQube installation the issues can be updated between projects directly. 6 | 7 | ##### Use cases: 8 | - Keeping resolved issues in sync between the master and a release/feature/maintenance branch 9 | - Using the list as a delivery for QA reports 10 | 11 | #### Matching issues, assignees and comments 12 | Issues are matched using the component, rule and linenumber. 13 | If an issue is matched it will be reported as 'matched'; if no transition can be determined to reach the exported status and resolution a 'matchFailure' will be reported. If the transition could not be succesfully completed a 'transitionFailure' will be reported. 14 | 15 | Assignee are matched using the username; the issue will be assigned to the assignee if the username is different and the issue will be reported as 'assigned'. If the assignment could not be succesfully completed an 'assignFailure' will be reported. 16 | 17 | Comments are matched by comparing the markdown. If a comment is not present on the issue it will be added and the issue will be reported as 'commented'. If a comment could not be succesfully added a 'commentFailure' will be reported. 18 | 19 | #### Resolving issues 20 | When transitioning, assigning issues or adding comments the current logged in account will be used. 21 | 22 | ## Usage 23 | - Install the plugin via the Update Center in the SonarQube administration pages. Or to install the plugin manually; copy the .jar file from the release to the `extensions/plugins` directory of your SonarQube installation. 24 | 25 | - Find the page 'Issue resolver' under the project Administration section. 26 | 27 | ##### Update 28 | - Select the plugin in the project you want to update issues in. You will need 'Browse' and 'Administer issues' (to resolve issues) permissions for this project. 29 | - Select the 'Update' tab. 30 | - Select the project you want to read issues from. You will need 'Browse' permission for this project. 31 | - Press the 'Update' button to read, match and resolve issues. 32 | 33 | ##### Export 34 | - Select the plugin in the project you want to export issues for. You will need 'Browse' and 'Administer issues' (to be able to reach the Administration section) permissions for this project. 35 | - Select the 'Export' tab. 36 | - Press the 'Export' button to download a datafile containing the resolved issues from the project. 37 | 38 | ##### Import 39 | - Select the plugin in the project you want to export issues for. You will need 'Browse' and 'Administer issues' (to resolve issues) permissions for this project. 40 | - Select the 'Export' tab. 41 | - Select the datafile containing the issues to import 42 | - Press the 'Import' button to upload the datafile and match and resolve issues. 43 | 44 | ##### Preview 45 | Use the preview option to preview the matching results. No actual changes will be made to the project. 46 | 47 | ## Webservices 48 | The main work for the plugin is done via webservices that are available via the SonarQube Web API (see SonarQube -> Helper -> Web API): 49 | 50 | - Update issues from another project: http POST to /api/issueresolver/update 51 | - Export issues from a project: http GET to /api/issueresolver/export 52 | - Import issues in a project: http POST to /api/issueresolver/import 53 | 54 | These webservices can be used by external tools to trigger the functionality. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | nl.future-edge.sonarqube.plugins 6 | sonar-issueresolver-plugin 7 | sonar-plugin 8 | 1.0.3-SNAPSHOT 9 | 10 | Issue resolver 11 | Export and import resolved issues (false-positive and won't fix) from SonarQube projects 12 | https://github.com/willemsrb/sonar-issueresolver-plugin 13 | 2017 14 | 15 | 16 | 17 | Apache License, Version 2.0 18 | https://www.apache.org/licenses/LICENSE-2.0.txt 19 | repo 20 | A business-friendly OSS license 21 | 22 | 23 | 24 | 25 | https://github.com/willemsrb/sonar-issueresolver-plugin/issues 26 | 27 | 28 | 29 | https://travis-ci.org/willemsrb/sonar-issueresolver-plugin 30 | 31 | 32 | 33 | https://github.com/willemsrb/sonar-issueresolver-plugin 34 | scm:git:https://github.com/willemsrb/sonar-issueresolver-plugin.git 35 | HEAD 36 | 37 | 38 | 39 | 40 | Robert Willems of Brilman 41 | https://github.com/willemsrb 42 | willemsrb 43 | 44 | 45 | 46 | 47 | 1.8 48 | UTF-8 49 | 50 | 6.3 51 | 3.13 52 | 53 | 54 | 55 | 56 | central 57 | Central Repository 58 | http://repo.maven.apache.org/maven2 59 | 60 | true 61 | 62 | 63 | false 64 | 65 | 66 | 67 | 68 | 69 | 70 | central 71 | Central Repository 72 | http://repo.maven.apache.org/maven2 73 | 74 | true 75 | 76 | 77 | false 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.sonarsource.sonarqube 85 | sonar-plugin-api 86 | ${sonar.version} 87 | provided 88 | 89 | 90 | 91 | org.sonarsource.sonarqube 92 | sonar-ws 93 | ${sonar.version} 94 | 95 | 96 | 97 | com.google.code.gson 98 | gson 99 | 2.3.1 100 | 101 | 102 | 103 | org.apache.commons 104 | commons-lang3 105 | 3.5 106 | 107 | 108 | 109 | 110 | junit 111 | junit 112 | 4.11 113 | test 114 | 115 | 116 | 117 | org.mockito 118 | mockito-core 119 | 2.7.19 120 | test 121 | 122 | 123 | 124 | 125 | org.sonarsource.orchestrator 126 | sonar-orchestrator 127 | ${sonar.orchestrator.version} 128 | test 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-compiler-plugin 138 | 139 | ${jdk.version} 140 | ${jdk.version} 141 | 142 | 143 | 144 | 145 | 146 | org.sonarsource.sonar-packaging-maven-plugin 147 | sonar-packaging-maven-plugin 148 | 1.18.0.372 149 | true 150 | 151 | nl.futureedge.sonar.plugin.issueresolver.IssueResolverPlugin 152 | 153 | 154 | 155 | 156 | 157 | org.apache.maven.plugins 158 | maven-resources-plugin 159 | 160 | 161 | copy-resources-for-it 162 | pre-integration-test 163 | 164 | copy-resources 165 | 166 | 167 | ${basedir}/target/it 168 | 169 | 170 | src/test/it 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-failsafe-plugin 180 | 181 | 182 | 183 | integration-test 184 | verify 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | org.apache.maven.plugins 195 | maven-compiler-plugin 196 | 3.6.1 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-resources-plugin 201 | 3.0.2 202 | 203 | 204 | org.apache.maven.plugins 205 | maven-surefire-plugin 206 | 2.19.1 207 | 208 | 209 | org.apache.maven.plugins 210 | maven-failsafe-plugin 211 | 2.19.1 212 | 213 | 214 | org.apache.maven.plugins 215 | maven-release-plugin 216 | 2.5.3 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPlugin.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver; 2 | 3 | import org.sonar.api.Plugin; 4 | import org.sonar.api.SonarQubeSide; 5 | import org.sonar.api.utils.log.Logger; 6 | import org.sonar.api.utils.log.Loggers; 7 | 8 | import nl.futureedge.sonar.plugin.issueresolver.page.IssueResolverPage; 9 | import nl.futureedge.sonar.plugin.issueresolver.ws.ExportAction; 10 | import nl.futureedge.sonar.plugin.issueresolver.ws.ImportAction; 11 | import nl.futureedge.sonar.plugin.issueresolver.ws.IssueResolverWebService; 12 | import nl.futureedge.sonar.plugin.issueresolver.ws.UpdateAction; 13 | 14 | /** 15 | * Issue resolver plugin. 16 | */ 17 | public final class IssueResolverPlugin implements Plugin { 18 | 19 | private static final Logger LOGGER = Loggers.get(ExportAction.class); 20 | 21 | @Override 22 | public void define(final Context context) { 23 | if (SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { 24 | LOGGER.info("Defining plugin ..."); 25 | context.addExtensions(ExportAction.class, ImportAction.class, UpdateAction.class); 26 | context.addExtensions(IssueResolverPage.class, IssueResolverWebService.class); 27 | LOGGER.info("Plugin defined"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/helper/IssueHelper.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.helper; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.function.BiConsumer; 6 | 7 | import org.sonar.api.server.ws.LocalConnector; 8 | import org.sonar.api.utils.log.Logger; 9 | import org.sonar.api.utils.log.Loggers; 10 | import org.sonarqube.ws.Issues.Comment; 11 | import org.sonarqube.ws.Issues.Issue; 12 | import org.sonarqube.ws.Issues.SearchWsResponse; 13 | import org.sonarqube.ws.client.PostRequest; 14 | import org.sonarqube.ws.client.WsClient; 15 | import org.sonarqube.ws.client.WsClientFactories; 16 | import org.sonarqube.ws.client.WsConnector; 17 | import org.sonarqube.ws.client.WsRequest; 18 | import org.sonarqube.ws.client.WsResponse; 19 | import org.sonarqube.ws.client.issue.SearchWsRequest; 20 | 21 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueData; 22 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueKey; 23 | import nl.futureedge.sonar.plugin.issueresolver.ws.ImportResult; 24 | 25 | /** 26 | * Issue functionality. 27 | */ 28 | public final class IssueHelper { 29 | 30 | private static final Logger LOGGER = Loggers.get(IssueHelper.class); 31 | 32 | private static final String PATH_TRANSITION = "api/issues/do_transition"; 33 | private static final String PATH_ASSIGN = "api/issues/assign"; 34 | private static final String PATH_ADD_COMMENT = "api/issues/add_comment"; 35 | 36 | private static final String PARAM_ISSUE = "issue"; 37 | private static final String PARAM_TRANSITION = "transition"; 38 | private static final String PARAM_ASSIGNEE = "assignee"; 39 | private static final String PARAM_TEXT = "text"; 40 | 41 | private IssueHelper() { 42 | } 43 | 44 | /** 45 | * Loop over issues. 46 | * 47 | * @param localConnector 48 | * local connector 49 | * @param searchIssuesRequest 50 | * search request 51 | * @param consumer 52 | * callback called for each issues 53 | */ 54 | public static void forEachIssue(final LocalConnector localConnector, final SearchWsRequest searchIssuesRequest, 55 | final BiConsumer consumer) { 56 | // Loop through all issues of the project 57 | final WsClient wsClient = WsClientFactories.getLocal().newClient(localConnector); 58 | 59 | boolean doNextPage = true; 60 | while (doNextPage) { 61 | LOGGER.debug("Listing issues for project {}; page {}", searchIssuesRequest.getProjectKeys(), 62 | searchIssuesRequest.getPage()); 63 | final SearchWsResponse searchIssuesResponse = wsClient.issues().search(searchIssuesRequest); 64 | for (final Issue issue : searchIssuesResponse.getIssuesList()) { 65 | consumer.accept(searchIssuesResponse, issue); 66 | } 67 | 68 | doNextPage = searchIssuesResponse.getPaging().getTotal() > (searchIssuesResponse.getPaging().getPageIndex() 69 | * searchIssuesResponse.getPaging().getPageSize()); 70 | searchIssuesRequest.setPage(searchIssuesResponse.getPaging().getPageIndex() + 1); 71 | searchIssuesRequest.setPageSize(searchIssuesResponse.getPaging().getPageSize()); 72 | } 73 | } 74 | 75 | /** 76 | * Resolve issues. 77 | * 78 | * @param localConnector 79 | * local connector 80 | * @param importResult 81 | * result 82 | * @param preview 83 | * true if issues should not be actually resolved. 84 | * @param skipAssign 85 | * if true, no assignments will be done 86 | * @param skipComments 87 | * if true, no comments will be added 88 | * @param projectKey 89 | * project key 90 | * @param issues 91 | * issues 92 | */ 93 | public static void resolveIssues(final LocalConnector localConnector, final ImportResult importResult, 94 | final boolean preview, final boolean skipAssign, final boolean skipComments, final String projectKey, 95 | final Map issues) { 96 | // Read issues from project, match and resolve 97 | importResult.setPreview(preview); 98 | 99 | final WsClient wsClient = WsClientFactories.getLocal().newClient(localConnector); 100 | 101 | // Loop through all issues of the project 102 | final SearchWsRequest searchIssuesRequest = SearchHelper.findIssuesForImport(projectKey); 103 | 104 | forEachIssue(localConnector, searchIssuesRequest, (searchIssuesResponse, issue) -> { 105 | final IssueKey key = IssueKey.fromIssue(issue, searchIssuesResponse.getComponentsList()); 106 | LOGGER.debug("Try to match issue: {}", key); 107 | // Match with issue from data 108 | final IssueData data = issues.remove(key); 109 | 110 | if (data != null) { 111 | importResult.registerMatchedIssue(); 112 | 113 | // Handle issue, if data is found 114 | handleTransition(wsClient.wsConnector(), issue, data.getStatus(), data.getResolution(), preview, 115 | importResult); 116 | if (!skipAssign) { 117 | handleAssignee(wsClient.wsConnector(), issue, data.getAssignee(), preview, importResult); 118 | } 119 | if (!skipComments) { 120 | handleComments(wsClient.wsConnector(), issue, data.getComments(), preview, importResult); 121 | } 122 | } 123 | }); 124 | } 125 | 126 | /* ************** ********** ************** */ 127 | /* ************** TRANSITION ************** */ 128 | /* ************** ********** ************** */ 129 | 130 | private static void handleTransition(final WsConnector wsConnector, final Issue issue, final String status, 131 | final String resolution, final boolean preview, final ImportResult importResult) { 132 | final String transition = determineTransition(issue.getKey(), issue.getStatus(), issue.getResolution(), status, 133 | resolution, importResult); 134 | if (transition != null) { 135 | if (!preview) { 136 | transitionIssue(wsConnector, issue.getKey(), transition, importResult); 137 | } 138 | importResult.registerTransitionedIssue(); 139 | } 140 | } 141 | 142 | private static String determineTransition(final String issue, final String currentStatus, 143 | final String currentResolution, final String wantedStatus, final String wantedResolution, 144 | final ImportResult importResult) { 145 | final String transition; 146 | if (TransitionHelper.noAction(currentStatus, currentResolution, wantedStatus, wantedResolution)) { 147 | transition = null; 148 | } else if (TransitionHelper.shouldConfirm(currentStatus, wantedStatus)) { 149 | transition = "confirm"; 150 | } else if (TransitionHelper.shouldUnconfirm(currentStatus, wantedStatus)) { 151 | transition = "unconfirm"; 152 | } else if (TransitionHelper.shouldReopen(currentStatus, wantedStatus)) { 153 | transition = "reopen"; 154 | } else if (TransitionHelper.shouldResolveFixed(currentStatus, wantedStatus, wantedResolution)) { 155 | transition = "resolve"; 156 | } else if (TransitionHelper.shouldResolveFalsePositive(currentStatus, wantedStatus, wantedResolution)) { 157 | transition = "falsepositive"; 158 | } else if (TransitionHelper.shouldReopen(currentStatus, wantedStatus, wantedResolution)) { 159 | transition = "wontfix"; 160 | } else { 161 | importResult.registerMatchFailure("Could not determine transition for issue with key '" + issue 162 | + "'; current status is '" + currentStatus + "' and resolution is '" + currentResolution 163 | + "'; wanted status is '" + wantedStatus + "' and resolution is '" + wantedResolution + "'"); 164 | transition = null; 165 | } 166 | return transition; 167 | } 168 | 169 | private static void transitionIssue(final WsConnector wsConnector, final String issue, final String transition, 170 | final ImportResult importResult) { 171 | final WsRequest request = new PostRequest(PATH_TRANSITION).setParam(PARAM_ISSUE, issue) 172 | .setParam(PARAM_TRANSITION, transition); 173 | final WsResponse response = wsConnector.call(request); 174 | 175 | if (!response.isSuccessful()) { 176 | LOGGER.debug("Failed to transition issue: " + response.content()); 177 | importResult.registerTransitionFailure( 178 | "Could not transition issue with key '" + issue + "' using transition '" + transition + "'"); 179 | } 180 | } 181 | 182 | /* ************** ******** ************** */ 183 | /* ************** ASSIGNEE ************** */ 184 | /* ************** ******** ************** */ 185 | 186 | private static void handleAssignee(final WsConnector wsConnector, final Issue issue, final String assignee, 187 | final boolean preview, final ImportResult importResult) { 188 | LOGGER.debug("Handle assignee '{}' for issue with key '{}'", assignee, issue.getKey()); 189 | final String currentAssignee = issue.getAssignee() == null ? "" : issue.getAssignee(); 190 | if (!currentAssignee.equals(assignee)) { 191 | if (!preview) { 192 | assignIssue(wsConnector, issue.getKey(), assignee, importResult); 193 | } 194 | importResult.registerAssignedIssue(); 195 | } 196 | } 197 | 198 | private static void assignIssue(final WsConnector wsConnector, final String issue, final String assignee, 199 | final ImportResult importResult) { 200 | LOGGER.debug("Assigning '{}' for issue with key '{}'", assignee, issue); 201 | final WsRequest request = new PostRequest(PATH_ASSIGN).setParam(PARAM_ISSUE, issue).setParam(PARAM_ASSIGNEE, 202 | assignee); 203 | final WsResponse response = wsConnector.call(request); 204 | 205 | if (!response.isSuccessful()) { 206 | LOGGER.debug("Failed to assign issue: " + response.content()); 207 | importResult.registerAssignFailure( 208 | "Could not assign issue with key '" + issue + "' to user '" + assignee + "'"); 209 | } 210 | } 211 | 212 | /* ************** ******* ************** */ 213 | /* ************** COMMENT ************** */ 214 | /* ************** ******* ************** */ 215 | 216 | private static void handleComments(final WsConnector wsConnector, final Issue issue, final List comments, 217 | final boolean preview, final ImportResult importResult) { 218 | boolean commentAdded = false; 219 | for (final String comment : comments) { 220 | if (!alreadyContainsComment(issue.getComments().getCommentsList(), comment)) { 221 | commentAdded = true; 222 | if (!preview) { 223 | addComment(wsConnector, issue.getKey(), comment, importResult); 224 | } 225 | } 226 | } 227 | 228 | if (commentAdded) { 229 | importResult.registerCommentedIssue(); 230 | } 231 | } 232 | 233 | private static boolean alreadyContainsComment(final List currentComments, final String comment) { 234 | for (Comment currentComment : currentComments) { 235 | if (currentComment.getMarkdown().equals(comment)) { 236 | return true; 237 | } 238 | } 239 | return false; 240 | } 241 | 242 | private static void addComment(final WsConnector wsConnector, final String issue, final String text, 243 | final ImportResult importResult) { 244 | final WsRequest request = new PostRequest(PATH_ADD_COMMENT).setParam(PARAM_ISSUE, issue).setParam(PARAM_TEXT, 245 | text); 246 | final WsResponse response = wsConnector.call(request); 247 | 248 | if (!response.isSuccessful()) { 249 | LOGGER.debug("Failed to add comment to issue: " + response.content()); 250 | importResult.registerCommentFailure("Could not add comment to issue with key '" + issue + "'"); 251 | } 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/helper/SearchHelper.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.helper; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | 6 | import org.sonarqube.ws.client.issue.SearchWsRequest; 7 | 8 | /** 9 | * Search functionality. 10 | */ 11 | public final class SearchHelper { 12 | 13 | private SearchHelper() { 14 | } 15 | 16 | /** 17 | * Create search request for resolved issues. 18 | * 19 | * @param projectKey 20 | * project key 21 | * @return search request 22 | */ 23 | public static SearchWsRequest findIssuesForExport(final String projectKey) { 24 | final SearchWsRequest searchIssuesRequest = new SearchWsRequest(); 25 | searchIssuesRequest.setProjectKeys(Collections.singletonList(projectKey)); 26 | searchIssuesRequest.setAdditionalFields(Collections.singletonList("comments")); 27 | searchIssuesRequest.setStatuses(Arrays.asList("CONFIRMED", "REOPENED", "RESOLVED")); 28 | searchIssuesRequest.setPage(1); 29 | searchIssuesRequest.setPageSize(100); 30 | return searchIssuesRequest; 31 | } 32 | 33 | public static SearchWsRequest findIssuesForImport(final String projectKey) { 34 | final SearchWsRequest searchIssuesRequest = new SearchWsRequest(); 35 | searchIssuesRequest.setProjectKeys(Collections.singletonList(projectKey)); 36 | searchIssuesRequest.setAdditionalFields(Collections.singletonList("comments")); 37 | searchIssuesRequest.setPage(1); 38 | searchIssuesRequest.setPageSize(100); 39 | return searchIssuesRequest; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/helper/TransitionHelper.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.helper; 2 | 3 | import java.util.Objects; 4 | 5 | import org.sonar.api.issue.Issue; 6 | 7 | /** 8 | * Transition helper. 9 | */ 10 | public final class TransitionHelper { 11 | 12 | private static final String OPEN = Issue.STATUS_OPEN; 13 | private static final String REOPENED = Issue.STATUS_REOPENED; 14 | private static final String CONFIRMED = Issue.STATUS_CONFIRMED; 15 | private static final String RESOLVED = Issue.STATUS_RESOLVED; 16 | 17 | private static final String FIXED = Issue.RESOLUTION_FIXED; 18 | private static final String FALSE_POSITIVE = Issue.RESOLUTION_FALSE_POSITIVE; 19 | private static final String WONT_FIX = Issue.RESOLUTION_WONT_FIX; 20 | 21 | private TransitionHelper() { 22 | } 23 | 24 | public static boolean noAction(final String currentStatus, final String currentResolution, 25 | final String wantedStatus, final String wantedResolution) { 26 | return noActionStatus(currentStatus, wantedStatus) && noActionResolution(currentResolution, wantedResolution); 27 | } 28 | 29 | private static boolean noActionStatus(final String currentStatus, final String wantedStatus) { 30 | final String current = REOPENED.equals(currentStatus) ? OPEN : currentStatus; 31 | final String wanted = REOPENED.equals(wantedStatus) ? OPEN : wantedStatus; 32 | 33 | return Objects.equals(current, wanted); 34 | } 35 | 36 | private static boolean noActionResolution(final String currentResolution, final String wantedResolution) { 37 | return Objects.equals(currentResolution, wantedResolution); 38 | } 39 | 40 | public static boolean shouldConfirm(final String currentStatus, final String wantedStatus) { 41 | return CONFIRMED.equals(wantedStatus) && (OPEN.equals(currentStatus) || REOPENED.equals(currentStatus)); 42 | } 43 | 44 | public static boolean shouldUnconfirm(final String currentStatus, final String wantedStatus) { 45 | return REOPENED.equals(wantedStatus) && CONFIRMED.equals(currentStatus); 46 | } 47 | 48 | public static boolean shouldReopen(final String currentStatus, final String wantedStatus) { 49 | return REOPENED.equals(wantedStatus) && RESOLVED.equals(currentStatus); 50 | } 51 | 52 | private static boolean shouldResolve(final String currentStatus, final String wantedStatus) { 53 | return RESOLVED.equals(wantedStatus) 54 | && (OPEN.equals(currentStatus) || REOPENED.equals(currentStatus) || CONFIRMED.equals(currentStatus)); 55 | } 56 | 57 | public static boolean shouldResolveFixed(final String currentStatus, final String wantedStatus, 58 | final String wantedResolution) { 59 | return shouldResolve(currentStatus, wantedStatus) && FIXED.equals(wantedResolution); 60 | } 61 | 62 | public static boolean shouldResolveFalsePositive(final String currentStatus, final String wantedStatus, 63 | final String wantedResolution) { 64 | return shouldResolve(currentStatus, wantedStatus) && FALSE_POSITIVE.equals(wantedResolution); 65 | } 66 | 67 | public static boolean shouldReopen(final String currentStatus, final String wantedStatus, 68 | final String wantedResolution) { 69 | return shouldResolve(currentStatus, wantedStatus) && WONT_FIX.equals(wantedResolution); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/helper/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers. 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver.helper; -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueData.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.issues; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.sonar.api.utils.text.JsonWriter; 8 | import org.sonarqube.ws.Issues.Comment; 9 | import org.sonarqube.ws.Issues.Issue; 10 | 11 | import nl.futureedge.sonar.plugin.issueresolver.json.JsonReader; 12 | 13 | /** 14 | * Issue data; used to resolve issues. 15 | */ 16 | public final class IssueData { 17 | 18 | private static final String NAME_STATUS = "status"; 19 | private static final String NAME_RESOLUTION = "resolution"; 20 | private static final String NAME_ASSIGNEE = "assignee"; 21 | private static final String NAME_COMMENTS = "comments"; 22 | 23 | private final String status; 24 | private final String resolution; 25 | private final String assignee; 26 | private final List comments; 27 | 28 | /** 29 | * Constructor. 30 | * 31 | * @param status 32 | * status 33 | * @param resolution 34 | * resolution 35 | * @param assignee 36 | * assignee 37 | * @param comments 38 | * comments 39 | */ 40 | private IssueData(final String status, final String resolution, final String assignee, 41 | final List comments) { 42 | this.status = status; 43 | this.resolution = resolution; 44 | this.assignee = assignee; 45 | this.comments = comments; 46 | } 47 | 48 | /** 49 | * Construct data from search. 50 | * 51 | * Reads the markdown format of comments. 52 | * 53 | * @param issue 54 | * issue from search 55 | * @return issue data 56 | */ 57 | public static IssueData fromIssue(final Issue issue) { 58 | final List comments = new ArrayList<>(); 59 | for (final Comment comment : issue.getComments().getCommentsList()) { 60 | comments.add(comment.getMarkdown()); 61 | } 62 | 63 | return new IssueData(issue.getStatus(), issue.getResolution(), issue.getAssignee(), comments); 64 | } 65 | 66 | /** 67 | * Construct data from export data. 68 | * 69 | * @param reader 70 | * json reader 71 | * @return issue data 72 | * @throws IOException 73 | * IO errors in underlying json reader 74 | */ 75 | public static IssueData read(final JsonReader reader) throws IOException { 76 | return new IssueData(reader.prop(NAME_STATUS), reader.prop(NAME_RESOLUTION), reader.prop(NAME_ASSIGNEE), 77 | reader.propValues(NAME_COMMENTS)); 78 | } 79 | 80 | /** 81 | * Write data to export data. 82 | * 83 | * @param writer 84 | * json writer 85 | */ 86 | public void write(final JsonWriter writer) { 87 | writer.prop(NAME_STATUS, status); 88 | writer.prop(NAME_RESOLUTION, resolution); 89 | writer.prop(NAME_ASSIGNEE, assignee); 90 | 91 | writer.name(NAME_COMMENTS); 92 | writer.beginArray(); 93 | writer.values(comments); 94 | writer.endArray(); 95 | } 96 | 97 | /** 98 | * Status. 99 | * 100 | * @return status 101 | */ 102 | public String getStatus() { 103 | return status; 104 | } 105 | 106 | 107 | /** 108 | * Resolution. 109 | * 110 | * @return resolution 111 | */ 112 | public String getResolution() { 113 | return resolution; 114 | } 115 | 116 | /** 117 | * Assignee. 118 | * 119 | * @return assignee 120 | */ 121 | public String getAssignee() { 122 | return assignee; 123 | } 124 | 125 | /** 126 | * Comments (markdown format). 127 | * 128 | * @return list of comments 129 | */ 130 | public List getComments() { 131 | return comments; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.issues; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | import org.apache.commons.lang3.builder.EqualsBuilder; 7 | import org.apache.commons.lang3.builder.HashCodeBuilder; 8 | import org.sonar.api.utils.text.JsonWriter; 9 | import org.sonarqube.ws.Issues.Component; 10 | import org.sonarqube.ws.Issues.Issue; 11 | 12 | import nl.futureedge.sonar.plugin.issueresolver.json.JsonReader; 13 | 14 | /** 15 | * Issue key; used to match issues. 16 | */ 17 | public final class IssueKey { 18 | 19 | private static final String NAME_LONG_NAME = "longName"; 20 | private static final String NAME_RULE = "rule"; 21 | private static final String NAME_LINE = "line"; 22 | 23 | private String longName; 24 | private String rule; 25 | private int line; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param rule 31 | * rule 32 | * @param component 33 | * component 34 | * @param line 35 | * line 36 | */ 37 | private IssueKey(final String longName, final String rule, final int line) { 38 | this.longName = longName; 39 | this.rule = rule; 40 | this.line = line; 41 | } 42 | 43 | /** 44 | * Construct key from search. 45 | * 46 | * @param issue 47 | * issue from search 48 | * @return issue key 49 | */ 50 | public static IssueKey fromIssue(final Issue issue, List components) { 51 | final Component component = findComponent(components, issue.getComponent()); 52 | return new IssueKey(component.getLongName(), issue.getRule(), issue.getTextRange().getStartLine()); 53 | } 54 | 55 | private static Component findComponent(final List components, final String key) { 56 | for (final Component component : components) { 57 | if (key.equals(component.getKey())) { 58 | return component; 59 | } 60 | } 61 | 62 | throw new IllegalStateException("Component of issue not found"); 63 | } 64 | 65 | /** 66 | * Construct key from export data. 67 | * 68 | * @param reader 69 | * json reader 70 | * @return issue key 71 | * @throws IOException 72 | * IO errors in underlying json reader 73 | */ 74 | public static IssueKey read(final JsonReader reader) throws IOException { 75 | return new IssueKey(reader.prop(NAME_LONG_NAME), reader.prop(NAME_RULE), reader.propAsInt(NAME_LINE)); 76 | } 77 | 78 | /** 79 | * Write key to export data. 80 | * 81 | * @param writer 82 | * json writer 83 | */ 84 | public void write(final JsonWriter writer) { 85 | writer.prop(NAME_LONG_NAME, longName); 86 | writer.prop(NAME_RULE, rule); 87 | writer.prop(NAME_LINE, line); 88 | } 89 | 90 | @Override 91 | public int hashCode() { 92 | return new HashCodeBuilder(17, 37).append(longName).append(rule).append(line).toHashCode(); 93 | } 94 | 95 | @Override 96 | public boolean equals(Object obj) { 97 | if (obj == null) { 98 | return false; 99 | } 100 | if (obj == this) { 101 | return true; 102 | } 103 | if (obj.getClass() != getClass()) { 104 | return false; 105 | } 106 | final IssueKey that = (IssueKey) obj; 107 | return new EqualsBuilder().append(longName, that.longName).append(rule, that.rule).append(line, that.line) 108 | .isEquals(); 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return "IssueKey [longName=" + longName + ", rule=" + rule + ", line=" + line + "]"; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Issues. 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver.issues; -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/json/JsonReader.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.json; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | import com.google.gson.stream.JsonToken; 12 | 13 | /** 14 | * JSON Reader. 15 | */ 16 | public final class JsonReader implements Closeable { 17 | private final com.google.gson.stream.JsonReader stream; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param inputStream 23 | * input stream 24 | * @throws IOException 25 | * on IO errors 26 | */ 27 | public JsonReader(final InputStream inputStream) throws IOException { 28 | stream = new com.google.gson.stream.JsonReader(new InputStreamReader(inputStream, "UTF-8")); 29 | } 30 | 31 | /** 32 | * Close. 33 | */ 34 | @Override 35 | public void close() throws IOException { 36 | stream.close(); 37 | } 38 | 39 | /** 40 | * Begin object. 41 | * 42 | * @throws IOException 43 | * on IO errors 44 | */ 45 | public void beginObject() throws IOException { 46 | stream.beginObject(); 47 | } 48 | 49 | /** 50 | * End object. 51 | * 52 | * @throws IOException 53 | * on IO errors 54 | */ 55 | public void endObject() throws IOException { 56 | stream.endObject(); 57 | } 58 | 59 | /** 60 | * Begin array. 61 | * 62 | * @throws IOException 63 | * on IO errors 64 | */ 65 | public void beginArray() throws IOException { 66 | stream.beginArray(); 67 | } 68 | 69 | /** 70 | * End array. 71 | * 72 | * @throws IOException 73 | * on IO errors 74 | */ 75 | public void endArray() throws IOException { 76 | stream.endArray(); 77 | } 78 | 79 | /** 80 | * Read a property; asserts it is the next property in the JSON stream. 81 | * 82 | * @param name 83 | * property name 84 | * @return property value 85 | * @throws IOException 86 | * on IO errors 87 | * @throws IllegalStateException 88 | * if the property isn't available 89 | */ 90 | public String prop(final String name) throws IOException { 91 | assertName(name); 92 | return stream.nextString(); 93 | } 94 | 95 | /** 96 | * Read a property as an integer; asserts it is the next property in the 97 | * JSON stream. 98 | * 99 | * @param name 100 | * property name 101 | * @return property value 102 | * @throws IOException 103 | * on IO errors 104 | * @throws IllegalStateException 105 | * if the property isn't available 106 | */ 107 | public int propAsInt(final String name) throws IOException { 108 | assertName(name); 109 | return stream.nextInt(); 110 | } 111 | 112 | /** 113 | * Read a property as a list of values; asserts it is the next property in 114 | * the JSON stream. 115 | * 116 | * @param name 117 | * property name 118 | * @return property value 119 | * @throws IOException 120 | * on IO errors 121 | * @throws IllegalStateException 122 | * if the property isn't available 123 | */ 124 | public List propValues(final String name) throws IOException { 125 | assertName(name); 126 | 127 | List result = new ArrayList<>(); 128 | stream.beginArray(); 129 | while (stream.hasNext()) { 130 | result.add(stream.nextString()); 131 | } 132 | 133 | stream.endArray(); 134 | return result; 135 | } 136 | 137 | /** 138 | * Assert the next property is of a given name. 139 | * 140 | * @param name 141 | * property name 142 | * @throws IOException 143 | * on IO errors 144 | * @throws IllegalStateException 145 | * if the property isn't available 146 | */ 147 | public void assertName(final String name) throws IOException { 148 | String actual = stream.nextName(); 149 | if (!Objects.equals(name, actual)) { 150 | throw new IllegalStateException("Expected name '" + name + "' but was '" + actual + "'"); 151 | } 152 | } 153 | 154 | /** 155 | * Checks if the stream has a next value. 156 | * 157 | * @return true if the stream contains a next value 158 | * @throws IOException 159 | * on IO errors 160 | */ 161 | public boolean hasNext() throws IOException { 162 | return stream.hasNext(); 163 | } 164 | 165 | /** 166 | * Checks if the stream contains nothing more. 167 | * 168 | * @return true, if the json stream contain nothing more 169 | * @throws IOException 170 | * on IO errors 171 | */ 172 | public boolean isEndOfDocument() throws IOException { 173 | return JsonToken.END_DOCUMENT == stream.peek(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/json/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON. 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver.json; -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Issue resolver plugin. 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver; 5 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/page/IssueResolverPage.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.page; 2 | 3 | import org.sonar.api.web.page.Context; 4 | import org.sonar.api.web.page.Page; 5 | import org.sonar.api.web.page.Page.Qualifier; 6 | import org.sonar.api.web.page.Page.Scope; 7 | import org.sonar.api.web.page.PageDefinition; 8 | 9 | /** 10 | * Issue resolver page. 11 | */ 12 | public final class IssueResolverPage implements PageDefinition { 13 | 14 | @Override 15 | public void define(final Context context) { 16 | final Page issueresolverPage = Page.builder("issueresolver/entrypoint").setName("Issue resolver") 17 | .setScope(Scope.COMPONENT).setComponentQualifiers(Qualifier.PROJECT).setAdmin(true).build(); 18 | context.addPage(issueresolverPage); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/page/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Page(s). 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver.page; -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/ExportAction.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.util.List; 4 | 5 | import org.sonar.api.server.ws.Request; 6 | import org.sonar.api.server.ws.Response; 7 | import org.sonar.api.server.ws.WebService.NewAction; 8 | import org.sonar.api.server.ws.WebService.NewController; 9 | import org.sonar.api.utils.log.Logger; 10 | import org.sonar.api.utils.log.Loggers; 11 | import org.sonar.api.utils.text.JsonWriter; 12 | import org.sonarqube.ws.Issues.Component; 13 | import org.sonarqube.ws.Issues.Issue; 14 | 15 | import nl.futureedge.sonar.plugin.issueresolver.helper.IssueHelper; 16 | import nl.futureedge.sonar.plugin.issueresolver.helper.SearchHelper; 17 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueData; 18 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueKey; 19 | 20 | /** 21 | * Export action. 22 | */ 23 | public class ExportAction implements IssueResolverWsAction { 24 | 25 | public static final String ACTION = "export"; 26 | public static final String PARAM_PROJECT_KEY = "projectKey"; 27 | 28 | private static final Logger LOGGER = Loggers.get(ExportAction.class); 29 | 30 | @Override 31 | public void define(final NewController controller) { 32 | LOGGER.debug("Defining export action ..."); 33 | final NewAction action = controller.createAction(ACTION) 34 | .setDescription("Export resolved issues with the status false positive or won't fix.") 35 | .setResponseExample(getClass().getResource("/response-examples/export.json")).setHandler(this) 36 | .setPost(false); 37 | 38 | action.createParam(PARAM_PROJECT_KEY).setDescription("Project to export issues for") 39 | .setExampleValue("my-project").setRequired(true); 40 | LOGGER.debug("Export action defined"); 41 | } 42 | 43 | @Override 44 | public void handle(final Request request, final Response response) { 45 | LOGGER.debug("Handle issueresolver export request"); 46 | response.setHeader("Content-Disposition", "attachment; filename=\"resolved-issues.json\""); 47 | 48 | final JsonWriter responseWriter = response.newJsonWriter(); 49 | writeStart(responseWriter); 50 | 51 | IssueHelper.forEachIssue(request.localConnector(), 52 | SearchHelper.findIssuesForExport(request.mandatoryParam(PARAM_PROJECT_KEY)), (searchIssuesResponse, 53 | issue) -> writeIssue(responseWriter, issue, searchIssuesResponse.getComponentsList())); 54 | 55 | writeEnd(responseWriter); 56 | responseWriter.close(); 57 | LOGGER.debug("Issueresolver export request done"); 58 | } 59 | 60 | private void writeStart(final JsonWriter writer) { 61 | writer.beginObject(); 62 | writer.prop("version", 1); 63 | writer.name("issues"); 64 | writer.beginArray(); 65 | } 66 | 67 | private void writeIssue(final JsonWriter writer, final Issue issue, List components) { 68 | writer.beginObject(); 69 | 70 | final IssueKey key = IssueKey.fromIssue(issue, components); 71 | key.write(writer); 72 | 73 | final IssueData data = IssueData.fromIssue(issue); 74 | data.write(writer); 75 | 76 | writer.endObject(); 77 | } 78 | 79 | private void writeEnd(final JsonWriter writer) { 80 | writer.endArray(); 81 | writer.endObject(); 82 | writer.close(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/ImportAction.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.io.IOException; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import org.sonar.api.server.ws.Request; 10 | import org.sonar.api.server.ws.Request.Part; 11 | import org.sonar.api.server.ws.Response; 12 | import org.sonar.api.server.ws.WebService.NewAction; 13 | import org.sonar.api.server.ws.WebService.NewController; 14 | import org.sonar.api.utils.log.Logger; 15 | import org.sonar.api.utils.log.Loggers; 16 | import org.sonar.api.utils.text.JsonWriter; 17 | 18 | import nl.futureedge.sonar.plugin.issueresolver.helper.IssueHelper; 19 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueData; 20 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueKey; 21 | import nl.futureedge.sonar.plugin.issueresolver.json.JsonReader; 22 | 23 | /** 24 | * Import action. 25 | */ 26 | public final class ImportAction implements IssueResolverWsAction { 27 | 28 | public static final String ACTION = "import"; 29 | public static final String PARAM_PROJECT_KEY = "projectKey"; 30 | public static final String PARAM_PREVIEW = "preview"; 31 | public static final String PARAM_DATA = "data"; 32 | public static final String PARAM_SKIP_ASSIGN = "skipAssign"; 33 | public static final String PARAM_SKIP_COMMENTS = "skipComments"; 34 | 35 | private static final String BOOLEAN_FALSE = "false"; 36 | private static final List BOOLEAN_VALUES = Arrays.asList("true", BOOLEAN_FALSE); 37 | 38 | private static final Logger LOGGER = Loggers.get(ImportAction.class); 39 | 40 | @Override 41 | public void define(final NewController controller) { 42 | LOGGER.debug("Defining import action ..."); 43 | final NewAction action = controller.createAction(ACTION) 44 | .setDescription("Import issues that have exported with the export function.") 45 | .setResponseExample(getClass().getResource("/response-examples/import.json")).setHandler(this) 46 | .setPost(true); 47 | 48 | action.createParam(PARAM_PROJECT_KEY).setDescription("Project to import issues to") 49 | .setExampleValue("my-project").setRequired(true); 50 | action.createParam(PARAM_PREVIEW).setDescription("If import should be a preview") 51 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 52 | action.createParam(PARAM_SKIP_ASSIGN).setDescription("If assignment should be skipped") 53 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 54 | action.createParam(PARAM_SKIP_COMMENTS).setDescription("If comments should be skipped") 55 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 56 | action.createParam(PARAM_DATA).setDescription("Exported resolved issue data").setRequired(true); 57 | LOGGER.debug("Import action defined"); 58 | } 59 | 60 | @Override 61 | public void handle(final Request request, final Response response) { 62 | LOGGER.info("Handle issueresolver import request ..."); 63 | final ImportResult importResult = new ImportResult(); 64 | 65 | // Read issue data from request 66 | final Map issues = readIssues(request, importResult); 67 | LOGGER.info("Read " + importResult.getIssues() + " issues (having " + importResult.getDuplicateKeys() 68 | + " duplicate keys)"); 69 | 70 | IssueHelper.resolveIssues(request.localConnector(), importResult, 71 | request.mandatoryParamAsBoolean(PARAM_PREVIEW), request.mandatoryParamAsBoolean(PARAM_SKIP_ASSIGN), 72 | request.mandatoryParamAsBoolean(PARAM_SKIP_COMMENTS), request.mandatoryParam(PARAM_PROJECT_KEY), 73 | issues); 74 | 75 | // Sent result 76 | final JsonWriter responseWriter = response.newJsonWriter(); 77 | importResult.write(responseWriter); 78 | responseWriter.close(); 79 | LOGGER.debug("Issueresolver import request done"); 80 | } 81 | 82 | /* ************** READ ************** */ 83 | /* ************** READ ************** */ 84 | /* ************** READ ************** */ 85 | 86 | private Map readIssues(final Request request, final ImportResult importResult) { 87 | final Part data = request.mandatoryParamAsPart(PARAM_DATA); 88 | final Map issues; 89 | 90 | try (final JsonReader reader = new JsonReader(data.getInputStream())) { 91 | reader.beginObject(); 92 | 93 | // Version 94 | final int version = reader.propAsInt("version"); 95 | switch (version) { 96 | case 1: 97 | issues = readIssuesVersionOne(reader, importResult); 98 | break; 99 | default: 100 | throw new IllegalStateException("Unknown version '" + version + "'"); 101 | } 102 | reader.endObject(); 103 | } catch (IOException e) { 104 | throw new IllegalStateException("Unexpected error during data parse", e); 105 | } 106 | return issues; 107 | } 108 | 109 | private Map readIssuesVersionOne(final JsonReader reader, final ImportResult importResult) 110 | throws IOException { 111 | final Map issues = new HashMap<>(); 112 | 113 | reader.assertName("issues"); 114 | reader.beginArray(); 115 | while (reader.hasNext()) { 116 | reader.beginObject(); 117 | final IssueKey key = IssueKey.read(reader); 118 | LOGGER.debug("Read issue: " + key); 119 | final IssueData data = IssueData.read(reader); 120 | importResult.registerIssue(); 121 | 122 | if (issues.containsKey(key)) { 123 | importResult.registerDuplicateKey(); 124 | } else { 125 | issues.put(key, data); 126 | } 127 | reader.endObject(); 128 | } 129 | reader.endArray(); 130 | 131 | return issues; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/ImportResult.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.sonar.api.utils.text.JsonWriter; 7 | 8 | /** 9 | * Import result. 10 | */ 11 | public final class ImportResult { 12 | 13 | private boolean preview = false; 14 | private int issues = 0; 15 | private int duplicateKeys = 0; 16 | private int matchedIssues = 0; 17 | private List matchFailures = new ArrayList<>(); 18 | private int transitionedIssues = 0; 19 | private List transitionFailures = new ArrayList<>(); 20 | private int assignedIssues = 0; 21 | private List assignFailures = new ArrayList<>(); 22 | private int commentedIssues = 0; 23 | private List commentFailures = new ArrayList<>(); 24 | 25 | public void setPreview(final boolean preview) { 26 | this.preview = preview; 27 | } 28 | 29 | public void registerIssue() { 30 | issues++; 31 | } 32 | 33 | public int getIssues() { 34 | return issues; 35 | } 36 | 37 | public void registerDuplicateKey() { 38 | duplicateKeys++; 39 | } 40 | 41 | public int getDuplicateKeys() { 42 | return duplicateKeys; 43 | } 44 | 45 | public void registerMatchedIssue() { 46 | matchedIssues++; 47 | } 48 | 49 | public void registerMatchFailure(String failure) { 50 | matchFailures.add(failure); 51 | } 52 | 53 | public void registerTransitionedIssue() { 54 | transitionedIssues++; 55 | } 56 | 57 | public void registerTransitionFailure(String failure) { 58 | transitionFailures.add(failure); 59 | } 60 | 61 | public void registerAssignedIssue() { 62 | assignedIssues++; 63 | } 64 | 65 | public void registerAssignFailure(String failure) { 66 | assignFailures.add(failure); 67 | } 68 | 69 | public void registerCommentedIssue() { 70 | commentedIssues++; 71 | } 72 | 73 | public void registerCommentFailure(String failure) { 74 | commentFailures.add(failure); 75 | } 76 | 77 | public void write(final JsonWriter writer) { 78 | writer.beginObject(); 79 | writer.prop("preview", preview); 80 | writer.prop("issues", issues); 81 | writer.prop("duplicateKeys", duplicateKeys); 82 | writer.prop("matchedIssues", matchedIssues); 83 | writer.name("matchFailures"); 84 | writer.beginArray(); 85 | writer.values(matchFailures); 86 | writer.endArray(); 87 | writer.prop("transitionedIssues", transitionedIssues); 88 | writer.name("transitionFailures"); 89 | writer.beginArray(); 90 | writer.values(transitionFailures); 91 | writer.endArray(); 92 | writer.prop("assignedIssues", assignedIssues); 93 | writer.name("assignFailures"); 94 | writer.beginArray(); 95 | writer.values(assignFailures); 96 | writer.endArray(); 97 | writer.prop("commentedIssues", commentedIssues); 98 | writer.name("commentFailures"); 99 | writer.beginArray(); 100 | writer.values(commentFailures); 101 | writer.endArray(); 102 | writer.endObject(); 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/IssueResolverWebService.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import org.sonar.api.server.ws.WebService; 4 | import org.sonar.api.utils.log.Logger; 5 | import org.sonar.api.utils.log.Loggers; 6 | 7 | /** 8 | * Issue resolver web service. 9 | */ 10 | public final class IssueResolverWebService implements WebService { 11 | 12 | /** 13 | * Controller path. 14 | */ 15 | public static final String CONTROLLER_PATH = "api/issueresolver"; 16 | 17 | private static final Logger LOGGER = Loggers.get(ExportAction.class); 18 | 19 | private final IssueResolverWsAction[] actions; 20 | 21 | /** 22 | * Constructor. 23 | * @param actions issue resolver actions 24 | */ 25 | public IssueResolverWebService(final IssueResolverWsAction... actions) { 26 | this.actions = actions; 27 | } 28 | 29 | @Override 30 | public void define(final Context context) { 31 | LOGGER.debug("Defining controller ..."); 32 | // Define the service 33 | final NewController controller = context.createController(CONTROLLER_PATH); 34 | controller.setDescription("Export and import resolved issues (false-positive and won't fix)"); 35 | 36 | // Define actions 37 | for (final IssueResolverWsAction action : actions) { 38 | action.define(controller); 39 | } 40 | 41 | // Apply changes 42 | controller.done(); 43 | LOGGER.debug("Controller defined"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/IssueResolverWsAction.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import org.sonar.api.server.ws.Definable; 4 | import org.sonar.api.server.ws.RequestHandler; 5 | import org.sonar.api.server.ws.WebService; 6 | 7 | /** 8 | * Issue resolver actions marker. 9 | */ 10 | public interface IssueResolverWsAction extends RequestHandler, Definable { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/UpdateAction.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import org.sonar.api.server.ws.Request; 9 | import org.sonar.api.server.ws.Response; 10 | import org.sonar.api.server.ws.WebService.NewAction; 11 | import org.sonar.api.server.ws.WebService.NewController; 12 | import org.sonar.api.utils.log.Logger; 13 | import org.sonar.api.utils.log.Loggers; 14 | import org.sonar.api.utils.text.JsonWriter; 15 | 16 | import nl.futureedge.sonar.plugin.issueresolver.helper.IssueHelper; 17 | import nl.futureedge.sonar.plugin.issueresolver.helper.SearchHelper; 18 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueData; 19 | import nl.futureedge.sonar.plugin.issueresolver.issues.IssueKey; 20 | 21 | /** 22 | * Update action. 23 | */ 24 | public final class UpdateAction implements IssueResolverWsAction { 25 | 26 | public static final String ACTION = "update"; 27 | public static final String PARAM_PROJECT_KEY = "projectKey"; 28 | public static final String PARAM_FROM_PROJECT_KEY = "fromProjectKey"; 29 | public static final String PARAM_PREVIEW = "preview"; 30 | public static final String PARAM_SKIP_ASSIGN = "skipAssign"; 31 | public static final String PARAM_SKIP_COMMENTS = "skipComments"; 32 | 33 | private static final String BOOLEAN_FALSE = "false"; 34 | private static final List BOOLEAN_VALUES = Arrays.asList("true", BOOLEAN_FALSE); 35 | 36 | private static final Logger LOGGER = Loggers.get(UpdateAction.class); 37 | 38 | @Override 39 | public void define(final NewController controller) { 40 | LOGGER.debug("Defining update action ..."); 41 | final NewAction action = controller.createAction(ACTION) 42 | .setDescription("Update issues from in one project based on another.") 43 | .setResponseExample(getClass().getResource("/response-examples/import.json")).setHandler(this) 44 | .setPost(true); 45 | 46 | action.createParam(PARAM_PROJECT_KEY).setDescription("Project to resolve issues in") 47 | .setExampleValue("my-project").setRequired(true); 48 | action.createParam(PARAM_FROM_PROJECT_KEY).setDescription("Project to read issues from") 49 | .setExampleValue("my-other-project").setRequired(true); 50 | action.createParam(PARAM_PREVIEW).setDescription("If import should be a preview") 51 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 52 | action.createParam(PARAM_SKIP_ASSIGN).setDescription("If assignment should be skipped") 53 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 54 | action.createParam(PARAM_SKIP_COMMENTS).setDescription("If comments should be skipped") 55 | .setPossibleValues(BOOLEAN_VALUES).setDefaultValue(BOOLEAN_FALSE); 56 | LOGGER.debug("Update action defined"); 57 | } 58 | 59 | @Override 60 | public void handle(final Request request, final Response response) { 61 | LOGGER.info("Handle issueresolver update request ..."); 62 | final ImportResult importResult = new ImportResult(); 63 | 64 | // Read issues from project 65 | final Map issues = new HashMap<>(); 66 | IssueHelper.forEachIssue(request.localConnector(), 67 | SearchHelper.findIssuesForExport(request.mandatoryParam(PARAM_FROM_PROJECT_KEY)), 68 | (searchIssuesResponse, issue) -> { 69 | issues.put(IssueKey.fromIssue(issue, searchIssuesResponse.getComponentsList()), 70 | IssueData.fromIssue(issue)); 71 | importResult.registerIssue(); 72 | }); 73 | LOGGER.info("Read " + importResult.getIssues() + " issues"); 74 | 75 | IssueHelper.resolveIssues(request.localConnector(), importResult, 76 | request.mandatoryParamAsBoolean(PARAM_PREVIEW), request.mandatoryParamAsBoolean(PARAM_SKIP_ASSIGN), 77 | request.mandatoryParamAsBoolean(PARAM_SKIP_COMMENTS), request.mandatoryParam(PARAM_PROJECT_KEY), 78 | issues); 79 | 80 | // Sent result 81 | final JsonWriter responseWriter = response.newJsonWriter(); 82 | importResult.write(responseWriter); 83 | responseWriter.close(); 84 | LOGGER.debug("Issueresolver update request done"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/nl/futureedge/sonar/plugin/issueresolver/ws/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Webservice(s). 3 | */ 4 | package nl.futureedge.sonar.plugin.issueresolver.ws; -------------------------------------------------------------------------------- /src/main/resources/response-examples/export.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "issues": [{ 4 | "longName": "src/main/java/TestClass.java", 5 | "rule": "squid:S1220", 6 | "line": 0, 7 | "status": "RESOLVED", 8 | "resolution": "WONTFIX", 9 | "assignee": "", 10 | "comments": [] 11 | }, 12 | { 13 | "longName": "src/main/java/TestClass.java", 14 | "rule": "squid:S106", 15 | "line": 6, 16 | "status": "RESOLVED", 17 | "resolution": "FALSE-POSITIVE", 18 | "assignee": "", 19 | "comments": [] 20 | }] 21 | } -------------------------------------------------------------------------------- /src/main/resources/response-examples/import.json: -------------------------------------------------------------------------------- 1 | { 2 | "preview": false, 3 | "issues": 10, 4 | "duplicateKeys": 1, 5 | "matchedIssues": 8, 6 | "matchFailures": ["Could not determine transition for issue with key 'issue-key-1'; current status is 'RESOLVED' and resolution is 'WONTFIX'; wanted status is 'RESOLVED' and resolution is 'FALSE-POSITIVE'"], 7 | "transitionedIssues": 6, 8 | "transitionFailures": ["Could not transition issue with key 'issue-key-2' using transition 'reopen'"], 9 | "assignedIssues": 2, 10 | "assignFailures": ["Could not assign issue with key 'issue-key-3' to user 'unknown'"], 11 | "commentedIssues": 1, 12 | "commentFailures": ["Could not add comment to issue with key 'issue-key-4'"] 13 | } -------------------------------------------------------------------------------- /src/main/resources/static/config.js: -------------------------------------------------------------------------------- 1 | define(function(){ 2 | return { 3 | basename: '' 4 | }; 5 | }); -------------------------------------------------------------------------------- /src/main/resources/static/dom.js: -------------------------------------------------------------------------------- 1 | define({ 2 | createElement: function (parent, name, properties) { 3 | var element = document.createElement(name); 4 | for(var propertyName in properties){ 5 | element[propertyName] = properties[propertyName]; 6 | } 7 | parent.appendChild(element); 8 | return element; 9 | }, 10 | 11 | removeChildren: function(parent) { 12 | while (parent.firstChild) { 13 | parent.removeChild(parent.firstChild); 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/main/resources/static/entrypoint.js: -------------------------------------------------------------------------------- 1 | window.registerExtension('issueresolver/entrypoint', function (options) { 2 | options.el.id='issueresolver-page'; 3 | options.el.className='page page-limited'; 4 | 5 | var location = options.router.createLocation('/static/issueresolver'); 6 | var loader = function() { 7 | requirejs.config({ 8 | baseUrl: location.basename + location.pathname 9 | }); 10 | requirejs(['config', 'main'], function(config, main) { 11 | config.basename = location.basename; 12 | main.main(options); 13 | }); 14 | }; 15 | 16 | // Adding the script tag to the head as suggested before 17 | var script = document.createElement('script'); 18 | script.type = 'text/javascript'; 19 | script.src = location.basename + location.pathname + '/require.js'; 20 | 21 | // Then bind the event to the callback function. 22 | // There are several events for cross browser compatibility. 23 | script.onreadystatechange = loader; 24 | script.onload = loader; 25 | 26 | // Fire the loading 27 | options.el.appendChild(script); 28 |   29 |   return function () { 30 | // No clean-up needed 31 |    }; 32 | }); -------------------------------------------------------------------------------- /src/main/resources/static/main.js: -------------------------------------------------------------------------------- 1 | define(['dom', 'tabsFactory', 'tabUpdate', 'tabExport', 'tabImport'], function(dom, tabsFactory, tabUpdate, tabExport, tabImport) { 2 | return { 3 | main: function(options) { 4 | var header = dom.createElement(options.el, 'header', { className: 'page-header'}); 5 | dom.createElement(header, 'h1', { className: 'page-title', textContent: 'Issue resolver'}); 6 | dom.createElement(header, 'div', { className: 'page-description', textContent: 'Allows you to export and import issues that are resolved with false positive and won\'t fix.'}); 7 | 8 | var tabs = tabsFactory.create(options.el); 9 | tabs.tab('Update', tabUpdate.create(options.component.key)); 10 | tabs.tab('Export', tabExport.create(options.component.key)); 11 | tabs.tab('Import', tabImport.create(options.component.key)); 12 | tabs.show('Update'); 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/main/resources/static/require.js: -------------------------------------------------------------------------------- 1 | /** vim: et:ts=4:sw=4:sts=4 2 | * @license RequireJS 2.3.3 Copyright jQuery Foundation and other contributors. 3 | * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE 4 | */ 5 | var requirejs,require,define;!function(global,setTimeout){function commentReplace(e,t){return t||""}function isFunction(e){return"[object Function]"===ostring.call(e)}function isArray(e){return"[object Array]"===ostring.call(e)}function each(e,t){if(e){var i;for(i=0;i-1&&(!e[i]||!t(e[i],i,e));i-=1);}}function hasProp(e,t){return hasOwn.call(e,t)}function getOwn(e,t){return hasProp(e,t)&&e[t]}function eachProp(e,t){var i;for(i in e)if(hasProp(e,i)&&t(e[i],i))break}function mixin(e,t,i,r){return t&&eachProp(t,function(t,n){!i&&hasProp(e,n)||(!r||"object"!=typeof t||!t||isArray(t)||isFunction(t)||t instanceof RegExp?e[n]=t:(e[n]||(e[n]={}),mixin(e[n],t,i,r)))}),e}function bind(e,t){return function(){return t.apply(e,arguments)}}function scripts(){return document.getElementsByTagName("script")}function defaultOnError(e){throw e}function getGlobal(e){if(!e)return e;var t=global;return each(e.split("."),function(e){t=t[e]}),t}function makeError(e,t,i,r){var n=new Error(t+"\nhttp://requirejs.org/docs/errors.html#"+e);return n.requireType=e,n.requireModules=r,i&&(n.originalError=i),n}function newContext(e){function t(e){var t,i;for(t=0;t0&&(e.splice(t-1,2),t-=2)}}function i(e,i,r){var n,o,a,s,u,c,d,p,f,l,h,m,g=i&&i.split("/"),v=y.map,x=v&&v["*"];if(e&&(e=e.split("/"),d=e.length-1,y.nodeIdCompat&&jsSuffixRegExp.test(e[d])&&(e[d]=e[d].replace(jsSuffixRegExp,"")),"."===e[0].charAt(0)&&g&&(m=g.slice(0,g.length-1),e=m.concat(e)),t(e),e=e.join("/")),r&&v&&(g||x)){a=e.split("/");e:for(s=a.length;s>0;s-=1){if(c=a.slice(0,s).join("/"),g)for(u=g.length;u>0;u-=1)if(o=getOwn(v,g.slice(0,u).join("/")),o&&(o=getOwn(o,c))){p=o,f=s;break e}!l&&x&&getOwn(x,c)&&(l=getOwn(x,c),h=s)}!p&&l&&(p=l,f=h),p&&(a.splice(0,f,p),e=a.join("/"))}return n=getOwn(y.pkgs,e),n?n:e}function r(e){isBrowser&&each(scripts(),function(t){if(t.getAttribute("data-requiremodule")===e&&t.getAttribute("data-requirecontext")===q.contextName)return t.parentNode.removeChild(t),!0})}function n(e){var t=getOwn(y.paths,e);if(t&&isArray(t)&&t.length>1)return t.shift(),q.require.undef(e),q.makeRequire(null,{skipMap:!0})([e]),!0}function o(e){var t,i=e?e.indexOf("!"):-1;return i>-1&&(t=e.substring(0,i),e=e.substring(i+1,e.length)),[t,e]}function a(e,t,r,n){var a,s,u,c,d=null,p=t?t.name:null,f=e,l=!0,h="";return e||(l=!1,e="_@r"+(T+=1)),c=o(e),d=c[0],e=c[1],d&&(d=i(d,p,n),s=getOwn(j,d)),e&&(d?h=r?e:s&&s.normalize?s.normalize(e,function(e){return i(e,p,n)}):e.indexOf("!")===-1?i(e,p,n):e:(h=i(e,p,n),c=o(h),d=c[0],h=c[1],r=!0,a=q.nameToUrl(h))),u=!d||s||r?"":"_unnormalized"+(A+=1),{prefix:d,name:h,parentMap:t,unnormalized:!!u,url:a,originalName:f,isDefine:l,id:(d?d+"!"+h:h)+u}}function s(e){var t=e.id,i=getOwn(S,t);return i||(i=S[t]=new q.Module(e)),i}function u(e,t,i){var r=e.id,n=getOwn(S,r);!hasProp(j,r)||n&&!n.defineEmitComplete?(n=s(e),n.error&&"error"===t?i(n.error):n.on(t,i)):"defined"===t&&i(j[r])}function c(e,t){var i=e.requireModules,r=!1;t?t(e):(each(i,function(t){var i=getOwn(S,t);i&&(i.error=e,i.events.error&&(r=!0,i.emit("error",e)))}),r||req.onError(e))}function d(){globalDefQueue.length&&(each(globalDefQueue,function(e){var t=e[0];"string"==typeof t&&(q.defQueueMap[t]=!0),O.push(e)}),globalDefQueue=[])}function p(e){delete S[e],delete k[e]}function f(e,t,i){var r=e.map.id;e.error?e.emit("error",e.error):(t[r]=!0,each(e.depMaps,function(r,n){var o=r.id,a=getOwn(S,o);!a||e.depMatched[n]||i[o]||(getOwn(t,o)?(e.defineDep(n,j[o]),e.check()):f(a,t,i))}),i[r]=!0)}function l(){var e,t,i=1e3*y.waitSeconds,o=i&&q.startTime+i<(new Date).getTime(),a=[],s=[],u=!1,d=!0;if(!x){if(x=!0,eachProp(k,function(e){var i=e.map,c=i.id;if(e.enabled&&(i.isDefine||s.push(e),!e.error))if(!e.inited&&o)n(c)?(t=!0,u=!0):(a.push(c),r(c));else if(!e.inited&&e.fetched&&i.isDefine&&(u=!0,!i.prefix))return d=!1}),o&&a.length)return e=makeError("timeout","Load timeout for modules: "+a,null,a),e.contextName=q.contextName,c(e);d&&each(s,function(e){f(e,{},{})}),o&&!t||!u||!isBrowser&&!isWebWorker||w||(w=setTimeout(function(){w=0,l()},50)),x=!1}}function h(e){hasProp(j,e[0])||s(a(e[0],null,!0)).init(e[1],e[2])}function m(e,t,i,r){e.detachEvent&&!isOpera?r&&e.detachEvent(r,t):e.removeEventListener(i,t,!1)}function g(e){var t=e.currentTarget||e.srcElement;return m(t,q.onScriptLoad,"load","onreadystatechange"),m(t,q.onScriptError,"error"),{node:t,id:t&&t.getAttribute("data-requiremodule")}}function v(){var e;for(d();O.length;){if(e=O.shift(),null===e[0])return c(makeError("mismatch","Mismatched anonymous define() module: "+e[e.length-1]));h(e)}q.defQueueMap={}}var x,b,q,E,w,y={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},S={},k={},M={},O=[],j={},P={},R={},T=1,A=1;return E={require:function(e){return e.require?e.require:e.require=q.makeRequire(e.map)},exports:function(e){if(e.usingExports=!0,e.map.isDefine)return e.exports?j[e.map.id]=e.exports:e.exports=j[e.map.id]={}},module:function(e){return e.module?e.module:e.module={id:e.map.id,uri:e.map.url,config:function(){return getOwn(y.config,e.map.id)||{}},exports:e.exports||(e.exports={})}}},b=function(e){this.events=getOwn(M,e.id)||{},this.map=e,this.shim=getOwn(y.shim,e.id),this.depExports=[],this.depMaps=[],this.depMatched=[],this.pluginMaps={},this.depCount=0},b.prototype={init:function(e,t,i,r){r=r||{},this.inited||(this.factory=t,i?this.on("error",i):this.events.error&&(i=bind(this,function(e){this.emit("error",e)})),this.depMaps=e&&e.slice(0),this.errback=i,this.inited=!0,this.ignore=r.ignore,r.enabled||this.enabled?this.enable():this.check())},defineDep:function(e,t){this.depMatched[e]||(this.depMatched[e]=!0,this.depCount-=1,this.depExports[e]=t)},fetch:function(){if(!this.fetched){this.fetched=!0,q.startTime=(new Date).getTime();var e=this.map;return this.shim?void q.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],bind(this,function(){return e.prefix?this.callPlugin():this.load()})):e.prefix?this.callPlugin():this.load()}},load:function(){var e=this.map.url;P[e]||(P[e]=!0,q.load(this.map.id,e))},check:function(){if(this.enabled&&!this.enabling){var e,t,i=this.map.id,r=this.depExports,n=this.exports,o=this.factory;if(this.inited){if(this.error)this.emit("error",this.error);else if(!this.defining){if(this.defining=!0,this.depCount<1&&!this.defined){if(isFunction(o)){if(this.events.error&&this.map.isDefine||req.onError!==defaultOnError)try{n=q.execCb(i,o,r,n)}catch(t){e=t}else n=q.execCb(i,o,r,n);if(this.map.isDefine&&void 0===n&&(t=this.module,t?n=t.exports:this.usingExports&&(n=this.exports)),e)return e.requireMap=this.map,e.requireModules=this.map.isDefine?[this.map.id]:null,e.requireType=this.map.isDefine?"define":"require",c(this.error=e)}else n=o;if(this.exports=n,this.map.isDefine&&!this.ignore&&(j[i]=n,req.onResourceLoad)){var a=[];each(this.depMaps,function(e){a.push(e.normalizedMap||e)}),req.onResourceLoad(q,this.map,a)}p(i),this.defined=!0}this.defining=!1,this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else hasProp(q.defQueueMap,i)||this.fetch()}},callPlugin:function(){var e=this.map,t=e.id,r=a(e.prefix);this.depMaps.push(r),u(r,"defined",bind(this,function(r){var n,o,d,f=getOwn(R,this.map.id),l=this.map.name,h=this.map.parentMap?this.map.parentMap.name:null,m=q.makeRequire(e.parentMap,{enableBuildCallback:!0});return this.map.unnormalized?(r.normalize&&(l=r.normalize(l,function(e){return i(e,h,!0)})||""),o=a(e.prefix+"!"+l,this.map.parentMap,!0),u(o,"defined",bind(this,function(e){this.map.normalizedMap=o,this.init([],function(){return e},null,{enabled:!0,ignore:!0})})),d=getOwn(S,o.id),void(d&&(this.depMaps.push(o),this.events.error&&d.on("error",bind(this,function(e){this.emit("error",e)})),d.enable()))):f?(this.map.url=q.nameToUrl(f),void this.load()):(n=bind(this,function(e){this.init([],function(){return e},null,{enabled:!0})}),n.error=bind(this,function(e){this.inited=!0,this.error=e,e.requireModules=[t],eachProp(S,function(e){0===e.map.id.indexOf(t+"_unnormalized")&&p(e.map.id)}),c(e)}),n.fromText=bind(this,function(i,r){var o=e.name,u=a(o),d=useInteractive;r&&(i=r),d&&(useInteractive=!1),s(u),hasProp(y.config,t)&&(y.config[o]=y.config[t]);try{req.exec(i)}catch(e){return c(makeError("fromtexteval","fromText eval for "+t+" failed: "+e,e,[t]))}d&&(useInteractive=!0),this.depMaps.push(u),q.completeLoad(o),m([o],n)}),void r.load(e.name,m,n,y))})),q.enable(r,this),this.pluginMaps[r.id]=r},enable:function(){k[this.map.id]=this,this.enabled=!0,this.enabling=!0,each(this.depMaps,bind(this,function(e,t){var i,r,n;if("string"==typeof e){if(e=a(e,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap),this.depMaps[t]=e,n=getOwn(E,e.id))return void(this.depExports[t]=n(this));this.depCount+=1,u(e,"defined",bind(this,function(e){this.undefed||(this.defineDep(t,e),this.check())})),this.errback?u(e,"error",bind(this,this.errback)):this.events.error&&u(e,"error",bind(this,function(e){this.emit("error",e)}))}i=e.id,r=S[i],hasProp(E,i)||!r||r.enabled||q.enable(e,this)})),eachProp(this.pluginMaps,bind(this,function(e){var t=getOwn(S,e.id);t&&!t.enabled&&q.enable(e,this)})),this.enabling=!1,this.check()},on:function(e,t){var i=this.events[e];i||(i=this.events[e]=[]),i.push(t)},emit:function(e,t){each(this.events[e],function(e){e(t)}),"error"===e&&delete this.events[e]}},q={config:y,contextName:e,registry:S,defined:j,urlFetched:P,defQueue:O,defQueueMap:{},Module:b,makeModuleMap:a,nextTick:req.nextTick,onError:c,configure:function(e){if(e.baseUrl&&"/"!==e.baseUrl.charAt(e.baseUrl.length-1)&&(e.baseUrl+="/"),"string"==typeof e.urlArgs){var t=e.urlArgs;e.urlArgs=function(e,i){return(i.indexOf("?")===-1?"?":"&")+t}}var i=y.shim,r={paths:!0,bundles:!0,config:!0,map:!0};eachProp(e,function(e,t){r[t]?(y[t]||(y[t]={}),mixin(y[t],e,!0,!0)):y[t]=e}),e.bundles&&eachProp(e.bundles,function(e,t){each(e,function(e){e!==t&&(R[e]=t)})}),e.shim&&(eachProp(e.shim,function(e,t){isArray(e)&&(e={deps:e}),!e.exports&&!e.init||e.exportsFn||(e.exportsFn=q.makeShimExports(e)),i[t]=e}),y.shim=i),e.packages&&each(e.packages,function(e){var t,i;e="string"==typeof e?{name:e}:e,i=e.name,t=e.location,t&&(y.paths[i]=e.location),y.pkgs[i]=e.name+"/"+(e.main||"main").replace(currDirRegExp,"").replace(jsSuffixRegExp,"")}),eachProp(S,function(e,t){e.inited||e.map.unnormalized||(e.map=a(t,null,!0))}),(e.deps||e.callback)&&q.require(e.deps||[],e.callback)},makeShimExports:function(e){function t(){var t;return e.init&&(t=e.init.apply(global,arguments)),t||e.exports&&getGlobal(e.exports)}return t},makeRequire:function(t,n){function o(i,r,u){var d,p,f;return n.enableBuildCallback&&r&&isFunction(r)&&(r.__requireJsBuild=!0),"string"==typeof i?isFunction(r)?c(makeError("requireargs","Invalid require call"),u):t&&hasProp(E,i)?E[i](S[t.id]):req.get?req.get(q,i,t,o):(p=a(i,t,!1,!0),d=p.id,hasProp(j,d)?j[d]:c(makeError("notloaded",'Module name "'+d+'" has not been loaded yet for context: '+e+(t?"":". Use require([])")))):(v(),q.nextTick(function(){v(),f=s(a(null,t)),f.skipMap=n.skipMap,f.init(i,r,u,{enabled:!0}),l()}),o)}return n=n||{},mixin(o,{isBrowser:isBrowser,toUrl:function(e){var r,n=e.lastIndexOf("."),o=e.split("/")[0],a="."===o||".."===o;return n!==-1&&(!a||n>1)&&(r=e.substring(n,e.length),e=e.substring(0,n)),q.nameToUrl(i(e,t&&t.id,!0),r,!0)},defined:function(e){return hasProp(j,a(e,t,!1,!0).id)},specified:function(e){return e=a(e,t,!1,!0).id,hasProp(j,e)||hasProp(S,e)}}),t||(o.undef=function(e){d();var i=a(e,t,!0),n=getOwn(S,e);n.undefed=!0,r(e),delete j[e],delete P[i.url],delete M[e],eachReverse(O,function(t,i){t[0]===e&&O.splice(i,1)}),delete q.defQueueMap[e],n&&(n.events.defined&&(M[e]=n.events),p(e))}),o},enable:function(e){var t=getOwn(S,e.id);t&&s(e).enable()},completeLoad:function(e){var t,i,r,o=getOwn(y.shim,e)||{},a=o.exports;for(d();O.length;){if(i=O.shift(),null===i[0]){if(i[0]=e,t)break;t=!0}else i[0]===e&&(t=!0);h(i)}if(q.defQueueMap={},r=getOwn(S,e),!t&&!hasProp(j,e)&&r&&!r.inited){if(!(!y.enforceDefine||a&&getGlobal(a)))return n(e)?void 0:c(makeError("nodefine","No define call for "+e,null,[e]));h([e,o.deps||[],o.exportsFn])}l()},nameToUrl:function(e,t,i){var r,n,o,a,s,u,c,d=getOwn(y.pkgs,e);if(d&&(e=d),c=getOwn(R,e))return q.nameToUrl(c,t,i);if(req.jsExtRegExp.test(e))s=e+(t||"");else{for(r=y.paths,n=e.split("/"),o=n.length;o>0;o-=1)if(a=n.slice(0,o).join("/"),u=getOwn(r,a)){isArray(u)&&(u=u[0]),n.splice(0,o,u);break}s=n.join("/"),s+=t||(/^data\:|^blob\:|\?/.test(s)||i?"":".js"),s=("/"===s.charAt(0)||s.match(/^[\w\+\.\-]+:/)?"":y.baseUrl)+s}return y.urlArgs&&!/^blob\:/.test(s)?s+y.urlArgs(e,s):s},load:function(e,t){req.load(q,e,t)},execCb:function(e,t,i,r){return t.apply(r,i)},onScriptLoad:function(e){if("load"===e.type||readyRegExp.test((e.currentTarget||e.srcElement).readyState)){interactiveScript=null;var t=g(e);q.completeLoad(t.id)}},onScriptError:function(e){var t=g(e);if(!n(t.id)){var i=[];return eachProp(S,function(e,r){0!==r.indexOf("_@r")&&each(e.depMaps,function(e){if(e.id===t.id)return i.push(r),!0})}),c(makeError("scripterror",'Script error for "'+t.id+(i.length?'", needed by: '+i.join(", "):'"'),e,[t.id]))}}},q.require=q.makeRequire(),q}function getInteractiveScript(){return interactiveScript&&"interactive"===interactiveScript.readyState?interactiveScript:(eachReverse(scripts(),function(e){if("interactive"===e.readyState)return interactiveScript=e}),interactiveScript)}var req,s,head,baseElement,dataMain,src,interactiveScript,currentlyAddingScript,mainScript,subPath,version="2.3.3",commentRegExp=/\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm,cjsRequireRegExp=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,jsSuffixRegExp=/\.js$/,currDirRegExp=/^\.\//,op=Object.prototype,ostring=op.toString,hasOwn=op.hasOwnProperty,isBrowser=!("undefined"==typeof window||"undefined"==typeof navigator||!window.document),isWebWorker=!isBrowser&&"undefined"!=typeof importScripts,readyRegExp=isBrowser&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,defContextName="_",isOpera="undefined"!=typeof opera&&"[object Opera]"===opera.toString(),contexts={},cfg={},globalDefQueue=[],useInteractive=!1;if("undefined"==typeof define){if("undefined"!=typeof requirejs){if(isFunction(requirejs))return;cfg=requirejs,requirejs=void 0}"undefined"==typeof require||isFunction(require)||(cfg=require,require=void 0),req=requirejs=function(e,t,i,r){var n,o,a=defContextName;return isArray(e)||"string"==typeof e||(o=e,isArray(t)?(e=t,t=i,i=r):e=[]),o&&o.context&&(a=o.context),n=getOwn(contexts,a),n||(n=contexts[a]=req.s.newContext(a)),o&&n.configure(o),n.require(e,t,i)},req.config=function(e){return req(e)},req.nextTick="undefined"!=typeof setTimeout?function(e){setTimeout(e,4)}:function(e){e()},require||(require=req),req.version=version,req.jsExtRegExp=/^\/|:|\?|\.js$/,req.isBrowser=isBrowser,s=req.s={contexts:contexts,newContext:newContext},req({}),each(["toUrl","undef","defined","specified"],function(e){req[e]=function(){var t=contexts[defContextName];return t.require[e].apply(t,arguments)}}),isBrowser&&(head=s.head=document.getElementsByTagName("head")[0],baseElement=document.getElementsByTagName("base")[0],baseElement&&(head=s.head=baseElement.parentNode)),req.onError=defaultOnError,req.createNode=function(e,t,i){var r=e.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");return r.type=e.scriptType||"text/javascript",r.charset="utf-8",r.async=!0,r},req.load=function(e,t,i){var r,n=e&&e.config||{};if(isBrowser)return r=req.createNode(n,t,i),r.setAttribute("data-requirecontext",e.contextName),r.setAttribute("data-requiremodule",t),!r.attachEvent||r.attachEvent.toString&&r.attachEvent.toString().indexOf("[native code")<0||isOpera?(r.addEventListener("load",e.onScriptLoad,!1),r.addEventListener("error",e.onScriptError,!1)):(useInteractive=!0,r.attachEvent("onreadystatechange",e.onScriptLoad)),r.src=i,n.onNodeCreated&&n.onNodeCreated(r,n,t,i),currentlyAddingScript=r,baseElement?head.insertBefore(r,baseElement):head.appendChild(r),currentlyAddingScript=null,r;if(isWebWorker)try{setTimeout(function(){},0),importScripts(i),e.completeLoad(t)}catch(r){e.onError(makeError("importscripts","importScripts failed for "+t+" at "+i,r,[t]))}},isBrowser&&!cfg.skipDataMain&&eachReverse(scripts(),function(e){if(head||(head=e.parentNode),dataMain=e.getAttribute("data-main"))return mainScript=dataMain,cfg.baseUrl||mainScript.indexOf("!")!==-1||(src=mainScript.split("/"),mainScript=src.pop(),subPath=src.length?src.join("/")+"/":"./",cfg.baseUrl=subPath),mainScript=mainScript.replace(jsSuffixRegExp,""),req.jsExtRegExp.test(mainScript)&&(mainScript=dataMain),cfg.deps=cfg.deps?cfg.deps.concat(mainScript):[mainScript],!0}),define=function(e,t,i){var r,n;"string"!=typeof e&&(i=t,t=e,e=null),isArray(t)||(i=t,t=null),!t&&isFunction(i)&&(t=[],i.length&&(i.toString().replace(commentRegExp,commentReplace).replace(cjsRequireRegExp,function(e,i){t.push(i)}),t=(1===i.length?["require"]:["require","exports","module"]).concat(t))),useInteractive&&(r=currentlyAddingScript||getInteractiveScript(),r&&(e||(e=r.getAttribute("data-requiremodule")),n=contexts[r.getAttribute("data-requirecontext")])),n?(n.defQueue.push([e,t,i]),n.defQueueMap[e]=!0):globalDefQueue.push([e,t,i])},define.amd={jQuery:!0},req.exec=function(text){return eval(text)},req(cfg)}}(this,"undefined"==typeof setTimeout?void 0:setTimeout); -------------------------------------------------------------------------------- /src/main/resources/static/result.js: -------------------------------------------------------------------------------- 1 | define(['dom'], function(dom) { 2 | return { 3 | formatFailures: function(type, failures) { 4 | var divFailures = document.createElement('div'); 5 | divFailures.style = "paddding-top: 0.5em;" 6 | dom.createElement(divFailures, 'span', { style: 'font-weight: bold; font-style: italic;', textContent: type+':'}); 7 | var ulFailures = dom.createElement(divFailures, 'ul', {}); 8 | ulFailures.style = "list-style-type: disc; padding-left: 1.5em;" 9 | failures.forEach(function(item) { 10 | dom.createElement(ulFailures, 'li', { textContent: item }); 11 | }); 12 | 13 | return divFailures; 14 | }, 15 | formatIssuesWouldHaveBeen: function(preview, size) { 16 | var result = ", " + size + " issue"; 17 | if(size > 1) { 18 | result = result + "s"; 19 | } 20 | result = result + " "; 21 | 22 | if(preview) { 23 | result = result + "would have"; 24 | } else if(size == 1) { 25 | result = result + "has"; 26 | } else { 27 | result = result + "have"; 28 | } 29 | 30 | result = result + " been "; 31 | return result; 32 | }, 33 | formatIssues: function(type, response) { 34 | var currentDate = new Date(); 35 | var resultText = ("0" + currentDate.getHours()).slice(-2) + ":" 36 | + ("0" + currentDate.getMinutes()).slice(-2) + "." 37 | + ("0" + currentDate.getSeconds()).slice(-2) + " - " + type + " succeeded"; 38 | 39 | // Issues 40 | resultText = resultText + "; " 41 | + response.issues + " issue" + (response.issues > 1 ? "s": "") + " read"; 42 | 43 | // Duplicate keys 44 | if(response.duplicateKeys>0) { 45 | resultText = resultText + " ("+ response.duplicateKeys+" duplicate keys)"; 46 | } 47 | 48 | // Matched issues 49 | resultText = resultText + this.formatIssuesWouldHaveBeen(false, response.matchedIssues) + "matched"; 50 | 51 | // Transitioned issues 52 | if(response.transitionedIssues > 0) { 53 | resultText = resultText + this.formatIssuesWouldHaveBeen(response.preview, response.transitionedIssues) + "resolved"; 54 | } 55 | 56 | // Assigned issues 57 | if(response.assignedIssues > 0) { 58 | resultText = resultText + this.formatIssuesWouldHaveBeen(response.preview, response.assignedIssues) + "assigned"; 59 | } 60 | 61 | // Commented issues 62 | if(response.commentedIssues > 0) { 63 | resultText = resultText + this.formatIssuesWouldHaveBeen(response.preview, response.commentedIssues) + "commented"; 64 | } 65 | 66 | resultText = resultText + "."; 67 | return resultText; 68 | }, 69 | formatResult: function (type, response) { 70 | var divResult = document.createElement('div'); 71 | 72 | // Base result 73 | var baseResult = this.formatIssues(type, response); 74 | dom.createElement(divResult, 'span', { style: 'font-weight:bold;', textContent: baseResult }); 75 | 76 | // Match failures 77 | if(response.matchFailures.length > 0) { 78 | divResult.appendChild(this.formatFailures('Matching failures', response.matchFailures)); 79 | } 80 | 81 | // Transition failures 82 | if(response.transitionFailures.length > 0) { 83 | divResult.appendChild(this.formatFailures('Transition failures', response.transitionFailures)); 84 | } 85 | 86 | // Assign failures 87 | if(response.assignFailures.length > 0) { 88 | divResult.appendChild(this.formatFailures('Assign failures', response.assignFailures)); 89 | } 90 | 91 | // Comment failures 92 | if(response.commentFailures.length > 0) { 93 | divResult.appendChild(this.formatFailures('Comment failures', response.commentFailures)); 94 | } 95 | 96 | return divResult; 97 | }, 98 | 99 | formatError: function(type, error) { 100 | var currentDate = new Date(); 101 | var resultText = ("0" + currentDate.getHours()).slice(-2) + ":" 102 | + ("0" + currentDate.getMinutes()).slice(-2) + "." 103 | + ("0" + currentDate.getSeconds()).slice(-2) + " - "+type+" failed"; 104 | 105 | resultText = resultText + "; " + error; 106 | return document.createTextNode(resultText); 107 | } 108 | }; 109 | }); 110 | -------------------------------------------------------------------------------- /src/main/resources/static/tabExport.js: -------------------------------------------------------------------------------- 1 | define(['config', 'dom'], function(config, dom) { 2 | return { 3 | create: function(projectKey) { 4 | return { 5 | projectKey: projectKey, 6 | show: function(parent) { 7 | dom.createElement(parent, 'h2', { className: 'issueresolver-header', textContent: 'Export'}); 8 | dom.createElement(parent, 'h2', { className: 'issueresolver-description big-spacer-bottom', textContent: 'Export issues that are resolved as false positive or won\'t fix as a data file.'}); 9 | 10 | // Export - form 11 | var formExport = dom.createElement( parent, 'form', { id: 'issueresolver-export-form' }); 12 | 13 | // Export - form - button 14 | var formExportButton = dom.createElement(formExport, 'div', { className: 'modal-field'}); 15 | dom.createElement(formExportButton, 'button', { textContent: 'Export'}); 16 | 17 | // Export - form - onsubmit 18 | formExport.onsubmit = function() { 19 | window.location = config.basename + 'api/issueresolver/export?projectKey=' + encodeURI(projectKey); 20 | return false; 21 | }; 22 | } 23 | }; 24 | } 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/main/resources/static/tabImport.js: -------------------------------------------------------------------------------- 1 | define(['dom', 'result'], function(dom, result) { 2 | return { 3 | create: function(projectKey) { 4 | return { 5 | projectKey: projectKey, 6 | show: function(parent) { 7 | // Header and description 8 | dom.createElement(parent, 'h2', { className: 'issueresolver-header', textContent: 'Import'}); 9 | dom.createElement(parent, 'h2', { className: 'issueresolver-description big-spacer-bottom', textContent: 'Import a datafile with issues (created using export), that will be matched to current issues using rule key, component and location.'}); 10 | 11 | // Import - form 12 | var formImport = dom.createElement(parent, 'form', { id: 'issueresolver-import-form' }); 13 | 14 | // Import - form - projectKey (hidden) 15 | dom.createElement(formImport, 'input', { id: 'issueresolver-import-projectKey', type:'hidden', name: 'projectKey', value: projectKey }); 16 | 17 | // Import - form - data 18 | var formImportData = dom.createElement(formImport, 'div', { className: 'modal-field'}); 19 | var formImportDataLabel = dom.createElement(formImportData, 'label', { for: 'issueresolver-import-data'}); 20 | formImportDataLabel.appendChild(document.createTextNode('Data')); 21 | dom.createElement(formImportDataLabel, 'em', { className:'mandatory',textContent: '*'}); 22 | dom.createElement(formImportData, 'input', { id: 'issueresolver-import-data', type:'file', name:'data'}); 23 | dom.createElement(formImportData, 'div', { className:'modal-field-description', textContent: 'The exported issue data'}); 24 | 25 | // Import - form - preview (checkbox, optional) 26 | var formImportPreview = dom.createElement(formImport, 'div', { className: 'modal-field' }); 27 | var formImportPreviewLabel = dom.createElement(formImportPreview, 'label', { for: 'issueresolver-import-preview' }); 28 | formImportPreviewLabel.appendChild(document.createTextNode('Preview')); 29 | dom.createElement(formImportPreview, 'input', { id: 'issueresolver-import-preview', type: 'checkbox', name: 'preview', value: 'true'}); 30 | dom.createElement(formImportPreview, 'div', { className: 'modal-field-description', textContent: 'If set, issues are not actually resolved, but only matched and checked, no changes are made' }); 31 | 32 | // Import - form - skipAssign (checkbox, optional) 33 | var formImportSkipAssign = dom.createElement(formImport, 'div', { className: 'modal-field' }); 34 | var formImportSkipAssignLabel = dom.createElement(formImportSkipAssign, 'label', { for: 'issueresolver-import-skipassign' }); 35 | formImportSkipAssignLabel.appendChild(document.createTextNode('Skip assignments')); 36 | dom.createElement(formImportSkipAssign, 'input', { id: 'issueresolver-import-skipassign', type: 'checkbox', name: 'skipAssign', value: 'true'}); 37 | dom.createElement(formImportSkipAssign, 'div', { className: 'modal-field-description', textContent: 'If set, issue assignments are skipped' }); 38 | 39 | // Import - form - skipComments (checkbox, optional) 40 | var formImportSkipComments = dom.createElement(formImport, 'div', { className: 'modal-field' }); 41 | var formImportSkipCommentsLabel = dom.createElement(formImportSkipComments, 'label', { for: 'issueresolver-import-skipcomments' }); 42 | formImportSkipCommentsLabel.appendChild(document.createTextNode('Skip comments')); 43 | dom.createElement(formImportSkipComments, 'input', { id: 'issueresolver-import-skipcomments', type: 'checkbox', name: 'skipComments', value: 'true'}); 44 | dom.createElement(formImportSkipComments, 'div', { className: 'modal-field-description', textContent: 'If set, issue comments are skipped' }); 45 | 46 | // Import - form - button 47 | var formImportButton = dom.createElement(formImport, 'div', { className: 'modal-field' }); 48 | var formImportButtonButton = dom.createElement(formImportButton, 'button', { textContent: 'Import' }); 49 | 50 | // Result placeholder 51 | var divImportResult = dom.createElement(parent, 'div', {}); 52 | divImportResult.style.display = 'none'; 53 | dom.createElement(divImportResult, 'h2', { className: 'issueresolver-header', textContent: 'Import result'}); 54 | 55 | // Import - form - onsubmit 56 | formImport.onsubmit = function() { 57 | formImportButtonButton.disabled=true; 58 | 59 | window.SonarRequest.postJSON( 60 | '/api/issueresolver/import', 61 | new FormData(formImport) 62 | ).then(function(response) { 63 | divImportResult.appendChild(result.formatResult('Import', response)); 64 | divImportResult.style.display='block'; 65 | formImportButtonButton.disabled=false; 66 | }).catch(function (error) { 67 | divImportResult.appendChild(result.formatError('Import', error)); 68 | divImportResult.style.display='block'; 69 | formImportButtonButton.disabled=false; 70 | }); 71 | 72 | return false; 73 | }; 74 | } 75 | }; 76 | } 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /src/main/resources/static/tabUpdate.js: -------------------------------------------------------------------------------- 1 | define(['dom', 'result'], function(dom, result) { 2 | return { 3 | create: function(projectKey) { 4 | return { 5 | projectKey: projectKey, 6 | show: function(parent) { 7 | // Header and description 8 | dom.createElement(parent, 'h2', { className: 'issueresolver-header', textContent: 'Update'}); 9 | dom.createElement(parent, 'h2', { className: 'issueresolver-description big-spacer-bottom', textContent: 'Update issues (from another project), that will be matched to current issues using rule key, component and location.'}); 10 | 11 | // Update - form 12 | var formUpdate = dom.createElement(parent, 'form', { id: 'issueresolver-update-form' }); 13 | 14 | // Update - form - projectKey (hidden) 15 | dom.createElement(formUpdate, 'input', { id: 'issueresolver-update-projectKey', type:'hidden', name: 'projectKey', value: projectKey }); 16 | 17 | // Update - form - project key 18 | var formUpdateProjectKey = dom.createElement(formUpdate, 'div', { className: 'modal-field' }); 19 | var formUpdateProjectKeyLabel = dom.createElement(formUpdateProjectKey, 'label', { for: 'issueresolver-update-fromprojectkey' }); 20 | formUpdateProjectKeyLabel.appendChild(document.createTextNode('Project')); 21 | dom.createElement(formUpdateProjectKeyLabel, 'em', { className:'mandatory', textContent:'*' }); 22 | var formUpdateProjectKeyInput = dom.createElement(formUpdateProjectKey, 'select', { id: 'issueresolver-update-fromprojectkey', name: 'fromProjectKey' }); 23 | dom.createElement(formUpdateProjectKey, 'div', {className:'modal-field-description', textContent: 'The project to update issues from'}); 24 | 25 | // Update - form - preview (checkbox, optional) 26 | var formUpdatePreview = dom.createElement(formUpdate, 'div', { className: 'modal-field' }); 27 | var formUpdatePreviewLabel = dom.createElement(formUpdatePreview, 'label', { for: 'issueresolver-update-preview' }); 28 | formUpdatePreviewLabel.appendChild(document.createTextNode('Preview')); 29 | dom.createElement(formUpdatePreview, 'input', { id: 'issueresolver-update-preview', type: 'checkbox', name: 'preview', value: 'true'}); 30 | dom.createElement(formUpdatePreview, 'div', { className: 'modal-field-description', textContent: 'If set, issues are not actually resolved, but only matched and checked, no changes are made' }); 31 | 32 | // Update - form - skipAssign (checkbox, optional) 33 | var formUpdateSkipAssign = dom.createElement(formUpdate, 'div', { className: 'modal-field' }); 34 | var formUpdateSkipAssignLabel = dom.createElement(formUpdateSkipAssign, 'label', { for: 'issueresolver-update-skipassign' }); 35 | formUpdateSkipAssignLabel.appendChild(document.createTextNode('Skip assignments')); 36 | dom.createElement(formUpdateSkipAssign, 'input', { id: 'issueresolver-update-skipassign', type: 'checkbox', name: 'skipAssign', value: 'true'}); 37 | dom.createElement(formUpdateSkipAssign, 'div', { className: 'modal-field-description', textContent: 'If set, issue assignments are skipped' }); 38 | 39 | // Update - form - skipComments (checkbox, optional) 40 | var formUpdateSkipComments = dom.createElement(formUpdate, 'div', { className: 'modal-field' }); 41 | var formUpdateSkipCommentsLabel = dom.createElement(formUpdateSkipComments, 'label', { for: 'issueresolver-update-skipcomments' }); 42 | formUpdateSkipCommentsLabel.appendChild(document.createTextNode('Skip comments')); 43 | dom.createElement(formUpdateSkipComments, 'input', { id: 'issueresolver-update-skipcomments', type: 'checkbox', name: 'skipComments', value: 'true'}); 44 | dom.createElement(formUpdateSkipComments, 'div', { className: 'modal-field-description', textContent: 'If set, issue comments are skipped' }); 45 | 46 | // Update - form - button 47 | var formUpdateButton = dom.createElement(formUpdate, 'div', { className: 'modal-field' }); 48 | var formUpdateButtonButton = dom.createElement(formUpdateButton, 'button', { textContent: 'Update' }); 49 | 50 | // Result placeholder 51 | var divUpdateResult = dom.createElement(parent, 'div', {}); 52 | divUpdateResult.style.display = 'none'; 53 | dom.createElement(divUpdateResult, 'h2', { className: 'issueresolver-header', textContent: 'Update result'}); 54 | 55 | // Update - form - onsubmit 56 | formUpdate.onsubmit = function() { 57 | formUpdateButtonButton.disabled=true; 58 | 59 | window.SonarRequest.postJSON( 60 | '/api/issueresolver/update', 61 | new FormData(formUpdate) 62 | ).then(function(response) { 63 | divUpdateResult.appendChild(result.formatResult('Update', response)); 64 | divUpdateResult.style.display='block'; 65 | formUpdateButtonButton.disabled=false; 66 | }).catch(function (error) { 67 | divUpdateResult.appendChild(result.formatError('Update', error)); 68 | divUpdateResult.style.display='block'; 69 | formUpdateButtonButton.disabled=false; 70 | }); 71 | 72 | return false; 73 | }; 74 | 75 | // Populate project key drop down list 76 | window.SonarRequest.postJSON( 77 | '/api/components/search', 78 | { 'ps':999999,'qualifiers':'TRK'} 79 | ).then(function(response) { 80 | for(var componentIndex = 0; componentIndex < response.components.length; componentIndex++) { 81 | var component = response.components[componentIndex]; 82 | dom.createElement(formUpdateProjectKeyInput, 'option', { value: component.key, textContent: component.name }); 83 | } 84 | }).catch(function(error) { 85 | // Nothing 86 | }); 87 | } 88 | }; 89 | } 90 | }; 91 | }); 92 | -------------------------------------------------------------------------------- /src/main/resources/static/tabsFactory.js: -------------------------------------------------------------------------------- 1 | define(['dom'], function(dom) { 2 | return { 3 | create: function(parent) { 4 | // Setup the tabs 5 | var divLayout = dom.createElement(parent, 'div', { className: 'settings-layout' }); 6 | var divSide = dom.createElement(divLayout, 'div', { className: 'settings-side'}); 7 | var divMain = dom.createElement(divLayout, 'div', { className: 'settings-main'}); 8 | var ulMenu = dom.createElement(divSide, 'ul', { className: 'settings-menu'}); 9 | 10 | // Return the tabs object 11 | return { 12 | tabParent: divMain, 13 | menuParent: ulMenu, 14 | links: [], 15 | show: function(name){ 16 | dom.removeChildren(this.tabParent); 17 | this.links.forEach(function(item) { 18 | if(item.name == name) { 19 | item.link.className = 'active'; 20 | item.tab.show(divMain); 21 | } else { 22 | item.link.className = ''; 23 | } 24 | }); 25 | }, 26 | tab: function(name, tab) { 27 | var li = dom.createElement(this.menuParent, 'li', {}); 28 | var a = dom.createElement(li, 'a', { textContent: name, href: '#' }); 29 | var thisObject = this; 30 | 31 | a.onclick = function() { 32 | thisObject.show(name); 33 | } 34 | 35 | this.links.push({ name: name, link: a, tab: tab }); 36 | }, 37 | }; 38 | }, 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/test/it/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | nl.future-edge.sonarqube.plugins 6 | sonar-issueresolver-plugin-it 7 | jar 8 | 1.0 9 | 10 | Test project for Integration Test 11 | 12 | 13 | 14 | central 15 | Central Repository 16 | http://repo.maven.apache.org/maven2 17 | 18 | true 19 | 20 | 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | central 29 | Central Repository 30 | http://repo.maven.apache.org/maven2 31 | 32 | true 33 | 34 | 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-compiler-plugin 45 | 3.6.1 46 | 47 | ${jdk.version} 48 | ${jdk.version} 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/it/src/main/java/TestClass.java: -------------------------------------------------------------------------------- 1 | public class TestClass { 2 | 3 | public void main(String[] args) { 4 | if(args != null) { 5 | for(String arg : args) { 6 | System.out.println("Your argument is " + arg); 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPluginTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.sonar.api.Plugin; 6 | import org.sonar.api.SonarQubeSide; 7 | import org.sonar.api.internal.SonarRuntimeImpl; 8 | import org.sonar.api.utils.Version; 9 | 10 | import nl.futureedge.sonar.plugin.issueresolver.IssueResolverPlugin; 11 | 12 | public class IssueResolverPluginTest { 13 | 14 | @Test 15 | public void test() { 16 | final IssueResolverPlugin subject = new IssueResolverPlugin(); 17 | final Plugin.Context context = new Plugin.Context(SonarRuntimeImpl.forSonarQube(Version.create(5, 6), SonarQubeSide.SERVER)); 18 | 19 | Assert.assertEquals(0, context.getExtensions().size()); 20 | subject.define(context); 21 | Assert.assertEquals(5, context.getExtensions().size()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/PluginIT.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Arrays; 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | 9 | import org.apache.http.HttpEntity; 10 | import org.apache.http.HttpHost; 11 | import org.apache.http.HttpResponse; 12 | import org.apache.http.HttpStatus; 13 | import org.apache.http.auth.AuthScope; 14 | import org.apache.http.auth.UsernamePasswordCredentials; 15 | import org.apache.http.client.AuthCache; 16 | import org.apache.http.client.CredentialsProvider; 17 | import org.apache.http.client.HttpClient; 18 | import org.apache.http.client.methods.HttpPost; 19 | import org.apache.http.client.protocol.HttpClientContext; 20 | import org.apache.http.entity.ContentType; 21 | import org.apache.http.entity.mime.HttpMultipartMode; 22 | import org.apache.http.entity.mime.MultipartEntityBuilder; 23 | import org.apache.http.entity.mime.content.ByteArrayBody; 24 | import org.apache.http.entity.mime.content.StringBody; 25 | import org.apache.http.impl.auth.BasicScheme; 26 | import org.apache.http.impl.client.BasicAuthCache; 27 | import org.apache.http.impl.client.BasicCredentialsProvider; 28 | import org.apache.http.impl.client.HttpClientBuilder; 29 | import org.apache.http.util.EntityUtils; 30 | import org.junit.After; 31 | import org.junit.Assert; 32 | import org.junit.Before; 33 | import org.junit.Test; 34 | import org.junit.runner.RunWith; 35 | import org.junit.runners.Parameterized; 36 | import org.junit.runners.Parameterized.Parameters; 37 | import org.sonar.api.utils.log.Logger; 38 | import org.sonar.api.utils.log.Loggers; 39 | import org.sonar.wsclient.connectors.ConnectionException; 40 | import org.sonar.wsclient.services.CreateQuery; 41 | import org.sonar.wsclient.services.Model; 42 | import org.sonar.wsclient.services.Query; 43 | 44 | import com.google.gson.JsonArray; 45 | import com.google.gson.JsonElement; 46 | import com.google.gson.JsonObject; 47 | import com.google.gson.JsonParser; 48 | import com.sonar.orchestrator.Orchestrator; 49 | import com.sonar.orchestrator.OrchestratorBuilder; 50 | import com.sonar.orchestrator.build.MavenBuild; 51 | import com.sonar.orchestrator.container.Server; 52 | import com.sonar.orchestrator.locator.FileLocation; 53 | 54 | import nl.futureedge.sonar.plugin.issueresolver.ws.ExportAction; 55 | import nl.futureedge.sonar.plugin.issueresolver.ws.ImportAction; 56 | import nl.futureedge.sonar.plugin.issueresolver.ws.IssueResolverWebService; 57 | import nl.futureedge.sonar.plugin.issueresolver.ws.UpdateAction; 58 | 59 | @RunWith(Parameterized.class) 60 | public class PluginIT { 61 | 62 | private static final Logger LOGGER = Loggers.get(ExportAction.class); 63 | 64 | private static final String RESULT_NO_ISSUES = "{\"version\":1,\"issues\":[]}"; 65 | private static final String RESULT_ISSUES = "{\"version\":1,\"issues\":[" 66 | + "{\"longName\":\"src/main/java/TestClass.java\",\"rule\":\"squid:S1220\",\"line\":0,\"status\":\"RESOLVED\",\"resolution\":\"WONTFIX\",\"assignee\":\"\",\"comments\":[]}," 67 | + "{\"longName\":\"src/main/java/TestClass.java\",\"rule\":\"squid:S106\",\"line\":6,\"status\":\"RESOLVED\",\"resolution\":\"FALSE-POSITIVE\",\"assignee\":\"\",\"comments\":[]}" 68 | + "]}"; 69 | private static final String RESULT_IMPORT = "{\"preview\":false,\"issues\":2,\"duplicateKeys\":0,\"matchedIssues\":2,\"matchFailures\":[],\"transitionedIssues\":2,\"transitionFailures\":[],\"assignedIssues\":0,\"assignFailures\":[],\"commentedIssues\":0,\"commentFailures\":[]}"; 70 | private static final String RESULT_UPDATE = "{\"preview\":false,\"issues\":2,\"duplicateKeys\":0,\"matchedIssues\":2,\"matchFailures\":[],\"transitionedIssues\":2,\"transitionFailures\":[],\"assignedIssues\":0,\"assignFailures\":[],\"commentedIssues\":0,\"commentFailures\":[]}"; 71 | 72 | @Parameters 73 | public static Collection sonarQubeVersions() { 74 | return Arrays.asList(new Object[][] { { "6.3" } }); 75 | } 76 | 77 | private final String sonarQubeVersion; 78 | private Orchestrator orchestrator; 79 | 80 | public PluginIT(final String sonarQubeVersion) { 81 | this.sonarQubeVersion = sonarQubeVersion; 82 | } 83 | 84 | @Before 85 | public void setupSonarQube() { 86 | System.getProperties().setProperty("sonar.runtimeVersion", sonarQubeVersion); 87 | 88 | final OrchestratorBuilder builder = Orchestrator.builderEnv(); 89 | builder.addPlugin(FileLocation.byWildcardMavenFilename(new File("target"), "sonar-issueresolver-plugin-*.jar")); 90 | builder.setOrchestratorProperty("javaVersion", "4.2.1").addPlugin("java"); 91 | 92 | // Enable debug logging for web components 93 | builder.setServerProperty("sonar.log.level.web", "DEBUG"); 94 | 95 | orchestrator = builder.build(); 96 | orchestrator.start(); 97 | } 98 | 99 | @After 100 | public void teardownSonarQube() { 101 | if (orchestrator != null) { 102 | orchestrator.stop(); 103 | } 104 | } 105 | 106 | public void runSonar(String branch) { 107 | final File pom = new File(new File(".", "target/it"), "pom.xml"); 108 | 109 | final MavenBuild install = MavenBuild.create(pom).setGoals("clean verify"); 110 | Assert.assertTrue("'clean verify' failed", orchestrator.executeBuild(install).isSuccess()); 111 | 112 | final HashMap sonarProperties = new HashMap<>(); 113 | sonarProperties.put("sonar.login", ""); 114 | sonarProperties.put("sonar.password", ""); 115 | sonarProperties.put("sonar.skip", "false"); 116 | sonarProperties.put("sonar.scanner.skip", "false"); 117 | 118 | if (branch != null) { 119 | sonarProperties.put("sonar.branch", branch); 120 | } 121 | 122 | final MavenBuild sonar = MavenBuild.create(pom).setGoals("sonar:sonar").setProperties(sonarProperties); 123 | Assert.assertTrue("'sonar:sonar' failed", orchestrator.executeBuild(sonar).isSuccess()); 124 | } 125 | 126 | @Test 127 | public void test() { 128 | // MASTER 129 | runSonar(null); 130 | final String masterProjectKey = "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin-it"; 131 | 132 | // Export issues (no issues expected) 133 | final String resultExportA = exportIssues(masterProjectKey); 134 | LOGGER.info("Result export A: {}", resultExportA); 135 | Assert.assertEquals(RESULT_NO_ISSUES, resultExportA); 136 | 137 | // Resolve issues 138 | resolveIssues(masterProjectKey); 139 | 140 | // Export issues (two issues expected) 141 | final String resultExportB = exportIssues(masterProjectKey); 142 | LOGGER.info("Result export B: {}", resultExportB); 143 | Assert.assertEquals(RESULT_ISSUES, resultExportB); 144 | 145 | // BRANCH 146 | runSonar("branchOne"); 147 | final String branchOneProjectKey = "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin-it:branchOne"; 148 | 149 | // Export issues (no issues expected) 150 | final String resultExportC = exportIssues(branchOneProjectKey); 151 | LOGGER.info("Result export C: {}", resultExportC); 152 | Assert.assertEquals(RESULT_NO_ISSUES, resultExportC); 153 | 154 | // Import issues from master export into branch 155 | final String resultImport = importResolvedIssues(branchOneProjectKey, resultExportB); 156 | LOGGER.info("Result import: {}", resultImport); 157 | Assert.assertEquals(RESULT_IMPORT, resultImport); 158 | 159 | // Export issues (two issues expected) 160 | final String resultExportD = exportIssues(branchOneProjectKey); 161 | LOGGER.info("Result export D: {}", resultExportD); 162 | Assert.assertEquals(RESULT_ISSUES, resultExportD); 163 | 164 | // SECOND BRANCH 165 | runSonar("branchTwo"); 166 | final String branchTwoProjectKey = "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin-it:branchTwo"; 167 | 168 | // Export issues (no issues expected) 169 | final String resultExportE = exportIssues(branchTwoProjectKey); 170 | LOGGER.info("Result export E: {}", resultExportE); 171 | Assert.assertEquals(RESULT_NO_ISSUES, resultExportE); 172 | 173 | // Import issues from master export into branch 174 | String resultUpdate = updateResolvedIssues(masterProjectKey, branchTwoProjectKey); 175 | LOGGER.info("Result update: {}", resultUpdate); 176 | Assert.assertEquals(RESULT_UPDATE, resultUpdate); 177 | 178 | // Export issues (two issues expected) 179 | final String resultExportF = exportIssues(branchTwoProjectKey); 180 | LOGGER.info("Result export F: {}", resultExportF); 181 | Assert.assertEquals(RESULT_ISSUES, resultExportF); 182 | } 183 | 184 | private String exportIssues(final String projectKey) { 185 | LOGGER.info("Exporting issues for project {}", projectKey); 186 | final ExportQuery exportQuery = new ExportQuery(projectKey); 187 | return orchestrator.getServer().getAdminWsClient().getConnector().execute(exportQuery); 188 | } 189 | 190 | private void resolveIssues(String projectKey) { 191 | LOGGER.info("Listing issues for {}", projectKey); 192 | final SearchIssuesQuery issuesQuery = new SearchIssuesQuery(projectKey); 193 | final String issuesResult = orchestrator.getServer().getAdminWsClient().getConnector().execute(issuesQuery); 194 | final JsonObject json = new JsonParser().parse(issuesResult).getAsJsonObject(); 195 | final JsonArray issues = json.get("issues").getAsJsonArray(); 196 | LOGGER.info("Project {} has {} issues", projectKey, issues.size()); 197 | for (final JsonElement issueElement : issues) { 198 | final JsonObject issue = issueElement.getAsJsonObject(); 199 | if ("squid:S1220".equals(issue.get("rule").getAsString())) { 200 | resolveIssue(issue.get("key").getAsString(), "wontfix"); 201 | } 202 | if ("squid:S106".equals(issue.get("rule").getAsString())) { 203 | resolveIssue(issue.get("key").getAsString(), "falsepositive"); 204 | } 205 | } 206 | } 207 | 208 | private void resolveIssue(String issueKey, String transition) { 209 | LOGGER.info("Resolving issue {} with transition {}", issueKey, transition); 210 | final ResolveIssueQuery resolveIssueQuery = new ResolveIssueQuery(issueKey, transition); 211 | Assert.assertNotNull(orchestrator.getServer().getAdminWsClient().getConnector().execute(resolveIssueQuery)); 212 | } 213 | 214 | private String importResolvedIssues(final String projectKey, final String data) { 215 | // Cannot use query because we use the fileupload 216 | LOGGER.info("Importing issues into project {}", projectKey); 217 | 218 | // Use httpclient 4 219 | final CredentialsProvider provider = new BasicCredentialsProvider(); 220 | provider.setCredentials(AuthScope.ANY, 221 | new UsernamePasswordCredentials(Server.ADMIN_LOGIN, Server.ADMIN_PASSWORD)); 222 | 223 | final AuthCache authCache = new BasicAuthCache(); 224 | authCache.put(new HttpHost("localhost", orchestrator.getServer().port()), new BasicScheme()); 225 | 226 | final HttpClient client = HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build(); 227 | 228 | final HttpClientContext context = HttpClientContext.create(); 229 | context.setCredentialsProvider(provider); 230 | context.setAuthCache(authCache); 231 | 232 | final HttpPost post = new HttpPost(orchestrator.getServer().getUrl() + "/" 233 | + IssueResolverWebService.CONTROLLER_PATH + "/" + ImportAction.ACTION); 234 | post.setHeader("Accept", "application/json"); 235 | 236 | // Set data 237 | final MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 238 | builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); 239 | builder.addPart("projectKey", new StringBody(projectKey, ContentType.MULTIPART_FORM_DATA)); 240 | builder.addPart("data", 241 | new ByteArrayBody(data.getBytes(), ContentType.MULTIPART_FORM_DATA, "resolved-issues.json")); 242 | post.setEntity(builder.build()); 243 | 244 | try { 245 | final HttpResponse response = client.execute(post, context); 246 | final HttpEntity entity = response.getEntity(); 247 | if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { 248 | return EntityUtils.toString(entity); 249 | } else { 250 | throw new ConnectionException("HTTP error: " + response.getStatusLine().getStatusCode() + ", msg: " 251 | + response.getStatusLine().getReasonPhrase() + ", query: " + post.toString()); 252 | } 253 | } catch (IOException e) { 254 | throw new ConnectionException("Query: " + post.getURI(), e); 255 | } finally { 256 | post.releaseConnection(); 257 | } 258 | } 259 | 260 | private String updateResolvedIssues(final String fromProjectKey, final String projectKey) { 261 | LOGGER.info("Updating issues from project {} to project {}", fromProjectKey, projectKey); 262 | final UpdateQuery updateQuery = new UpdateQuery(fromProjectKey, projectKey); 263 | return orchestrator.getServer().getAdminWsClient().getConnector().execute(updateQuery); 264 | } 265 | 266 | /** 267 | * Export issues. 268 | */ 269 | private final class ExportQuery extends Query { 270 | 271 | public static final String BASE_URL = "/" + IssueResolverWebService.CONTROLLER_PATH + "/" + ExportAction.ACTION; 272 | 273 | private String projectKey; 274 | 275 | public ExportQuery(final String projectKey) { 276 | this.projectKey = projectKey; 277 | } 278 | 279 | @Override 280 | public Class getModelClass() { 281 | return Model.class; 282 | } 283 | 284 | @Override 285 | public String getUrl() { 286 | final StringBuilder url = new StringBuilder(BASE_URL); 287 | url.append('?'); 288 | appendUrlParameter(url, ExportAction.PARAM_PROJECT_KEY, projectKey); 289 | return url.toString(); 290 | } 291 | } 292 | 293 | /** 294 | * Search issues. 295 | */ 296 | private final class SearchIssuesQuery extends Query { 297 | 298 | public static final String BASE_URL = "/api/issues/search"; 299 | 300 | private String projectKey; 301 | 302 | public SearchIssuesQuery(final String projectKey) { 303 | this.projectKey = projectKey; 304 | } 305 | 306 | @Override 307 | public Class getModelClass() { 308 | return Model.class; 309 | } 310 | 311 | @Override 312 | public String getUrl() { 313 | final StringBuilder url = new StringBuilder(BASE_URL); 314 | url.append('?'); 315 | appendUrlParameter(url, "projectKeys", projectKey); 316 | return url.toString(); 317 | } 318 | } 319 | 320 | /** 321 | * Resolve issues. 322 | */ 323 | private final class ResolveIssueQuery extends CreateQuery { 324 | 325 | public static final String BASE_URL = "/api/issues/do_transition"; 326 | 327 | private String issueKey; 328 | private String transition; 329 | 330 | public ResolveIssueQuery(final String issueKey, final String transition) { 331 | this.issueKey = issueKey; 332 | this.transition = transition; 333 | } 334 | 335 | @Override 336 | public Class getModelClass() { 337 | return Model.class; 338 | } 339 | 340 | @Override 341 | public String getUrl() { 342 | final StringBuilder url = new StringBuilder(BASE_URL); 343 | url.append('?'); 344 | appendUrlParameter(url, "issue", issueKey); 345 | appendUrlParameter(url, "transition", transition); 346 | return url.toString(); 347 | } 348 | } 349 | 350 | /** 351 | * Update issues. 352 | */ 353 | private final class UpdateQuery extends CreateQuery { 354 | 355 | public static final String BASE_URL = "/" + IssueResolverWebService.CONTROLLER_PATH + "/" + UpdateAction.ACTION; 356 | 357 | private String fromProjectKey; 358 | private String projectKey; 359 | 360 | public UpdateQuery(final String fromProjectKey, final String projectKey) { 361 | this.fromProjectKey = fromProjectKey; 362 | this.projectKey = projectKey; 363 | } 364 | 365 | @Override 366 | public Class getModelClass() { 367 | return Model.class; 368 | } 369 | 370 | @Override 371 | public String getUrl() { 372 | final StringBuilder url = new StringBuilder(BASE_URL); 373 | url.append('?'); 374 | appendUrlParameter(url, UpdateAction.PARAM_FROM_PROJECT_KEY, fromProjectKey); 375 | appendUrlParameter(url, UpdateAction.PARAM_PROJECT_KEY, projectKey); 376 | return url.toString(); 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueDataTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.issues; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.StringWriter; 6 | import java.util.Arrays; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.sonar.api.utils.text.JsonWriter; 11 | import org.sonarqube.ws.Issues.Comment; 12 | import org.sonarqube.ws.Issues.Comments; 13 | import org.sonarqube.ws.Issues.Issue; 14 | 15 | import nl.futureedge.sonar.plugin.issueresolver.json.JsonReader; 16 | 17 | public class IssueDataTest { 18 | 19 | @Test 20 | public void test() throws IOException { 21 | final Issue issue = ReflectionTestUtils.build(Issue.class, "status_", "RESOLVED", "resolution_", "FALSE-POSITIVE", 22 | "assignee_", "admin", "comments_", 23 | ReflectionTestUtils.build(Comments.class, "comments_", 24 | Arrays.asList(ReflectionTestUtils.build(Comment.class, "markdown_", "Comment one"), 25 | ReflectionTestUtils.build(Comment.class, "markdown_", "Comment two")))); 26 | 27 | final IssueData data = IssueData.fromIssue(issue); 28 | Assert.assertEquals("RESOLVED", data.getStatus()); 29 | Assert.assertEquals("FALSE-POSITIVE", data.getResolution()); 30 | Assert.assertEquals("admin", data.getAssignee()); 31 | Assert.assertEquals(Arrays.asList("Comment one", "Comment two"), data.getComments()); 32 | 33 | final String json; 34 | try (final StringWriter writer = new StringWriter()) { 35 | final JsonWriter jsonWriter = JsonWriter.of(writer); 36 | jsonWriter.beginObject(); 37 | data.write(jsonWriter); 38 | jsonWriter.endObject(); 39 | jsonWriter.close(); 40 | 41 | json = writer.toString(); 42 | } 43 | Assert.assertEquals("{\"status\":\"RESOLVED\",\"resolution\":\"FALSE-POSITIVE\",\"assignee\":\"admin\",\"comments\":[\"Comment one\",\"Comment two\"]}", json); 44 | 45 | final IssueData readData; 46 | try (final ByteArrayInputStream bais = new ByteArrayInputStream(json.getBytes("UTF-8")); 47 | final JsonReader reader = new JsonReader(bais)) { 48 | reader.beginObject(); 49 | readData = IssueData.read(reader); 50 | reader.endObject(); 51 | } 52 | 53 | Assert.assertEquals("RESOLVED", readData.getStatus()); 54 | Assert.assertEquals("FALSE-POSITIVE", readData.getResolution()); 55 | Assert.assertEquals("admin", readData.getAssignee()); 56 | Assert.assertEquals(Arrays.asList("Comment one", "Comment two"), readData.getComments()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKeyTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.issues; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.StringWriter; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import org.junit.Assert; 10 | import org.junit.Test; 11 | import org.sonar.api.utils.text.JsonWriter; 12 | import org.sonarqube.ws.Common.TextRange; 13 | import org.sonarqube.ws.Issues.Component; 14 | import org.sonarqube.ws.Issues.Issue; 15 | 16 | import nl.futureedge.sonar.plugin.issueresolver.json.JsonReader; 17 | 18 | public class IssueKeyTest { 19 | 20 | @Test 21 | public void test() throws IOException { 22 | final Issue issue = ReflectionTestUtils.build(Issue.class, "rule_", "test:rule001", "component_", 23 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 24 | "textRange_", ReflectionTestUtils.build(TextRange.class, "startLine_", 13, "startOffset_", 65)); 25 | final Component component = ReflectionTestUtils.build(Component.class, "key_", "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 26 | "longName_", "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java"); 27 | final List components = Arrays.asList(component); 28 | 29 | final IssueKey key = IssueKey.fromIssue(issue, components); 30 | 31 | final String json; 32 | try (final StringWriter writer = new StringWriter()) { 33 | final JsonWriter jsonWriter = JsonWriter.of(writer); 34 | jsonWriter.beginObject(); 35 | key.write(jsonWriter); 36 | jsonWriter.endObject(); 37 | jsonWriter.close(); 38 | 39 | json = writer.toString(); 40 | } 41 | Assert.assertEquals( 42 | "{\"longName\":\"src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java\",\"rule\":\"test:rule001\",\"line\":13}", 43 | json); 44 | 45 | final IssueKey readKey; 46 | try (final ByteArrayInputStream bais = new ByteArrayInputStream(json.getBytes("UTF-8")); 47 | final JsonReader reader = new JsonReader(bais)) { 48 | reader.beginObject(); 49 | readKey = IssueKey.read(reader); 50 | reader.endObject(); 51 | } 52 | 53 | Assert.assertEquals(key.hashCode(), readKey.hashCode()); 54 | Assert.assertEquals(key, readKey); 55 | Assert.assertFalse(key.equals(null)); 56 | Assert.assertTrue(key.equals(key)); 57 | Assert.assertFalse(key.equals(new Object())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/issues/ReflectionTestUtils.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.issues; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Field; 5 | 6 | public class ReflectionTestUtils { 7 | 8 | public static T build(final Class clazz, Object... fields) { 9 | T instance = newInstance(clazz); 10 | 11 | if (fields != null) { 12 | for (int i = 0; i+1 < fields.length; i = i + 2) { 13 | setField(instance, (String)fields[i], fields[i+1]); 14 | } 15 | } 16 | return instance; 17 | } 18 | 19 | public static T newInstance(final Class clazz) { 20 | try { 21 | final Constructor constructor = clazz.getDeclaredConstructor(); 22 | constructor.setAccessible(true); 23 | 24 | return constructor.newInstance(); 25 | } catch (ReflectiveOperationException e) { 26 | throw new IllegalStateException(e); 27 | } 28 | } 29 | 30 | public static void setField(final Object object, final String name, final Object value) { 31 | try { 32 | final Field field = findField(object.getClass(), name); 33 | field.setAccessible(true); 34 | field.set(object, value); 35 | } catch (ReflectiveOperationException e) { 36 | throw new IllegalStateException(e); 37 | } 38 | } 39 | 40 | private static Field findField(final Class clazz, final String name) throws ReflectiveOperationException { 41 | Class theClazz = clazz; 42 | while (theClazz != null) { 43 | try { 44 | return theClazz.getDeclaredField(name); 45 | } catch (NoSuchFieldException e) { 46 | theClazz = theClazz.getSuperclass(); 47 | } 48 | } 49 | 50 | throw new NoSuchFieldException(name); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/json/JsonReaderTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.json; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | 9 | import org.junit.Assert; 10 | import org.junit.Test; 11 | 12 | public class JsonReaderTest { 13 | 14 | private static final String JSON = "{\"version\":1,\"issues\":[{\"rule\":\"squid:S1161\",\"component\":\"nl.future-edge.sonarqube" 15 | + ".plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPage.java\"," 16 | + "\"line\":13,\"offset\":15,\"resolution\":\"falsepositive\",\"comments\":[\"One\", \"Two\"]},{\"rule\":\"squid:S1161\",\"component\":\"nl.future-edge.sonarqube" 17 | + ".plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPage.java\"," 18 | + "\"line\":17,\"offset\":15,\"resolution\":\"wontfix\",\"comments\":[]}]}"; 19 | 20 | @Test 21 | public void test() throws IOException { 22 | try (InputStream inputStream = new ByteArrayInputStream(JSON.getBytes("UTF-8")); 23 | JsonReader reader = new JsonReader(inputStream)) { 24 | reader.beginObject(); 25 | Assert.assertEquals(1, reader.propAsInt("version")); 26 | reader.assertName("issues"); 27 | reader.beginArray(); 28 | // Issue 1 29 | Assert.assertTrue(reader.hasNext()); 30 | reader.beginObject(); 31 | Assert.assertEquals("squid:S1161", reader.prop("rule")); 32 | Assert.assertEquals( 33 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPage.java", 34 | reader.prop("component")); 35 | Assert.assertEquals(13, reader.propAsInt("line")); 36 | Assert.assertEquals(15, reader.propAsInt("offset")); 37 | Assert.assertEquals("falsepositive", reader.prop("resolution")); 38 | Assert.assertEquals(Arrays.asList("One", "Two"), reader.propValues("comments")); 39 | Assert.assertFalse(reader.hasNext()); 40 | reader.endObject(); 41 | 42 | // Issue 2 43 | Assert.assertTrue(reader.hasNext()); 44 | reader.beginObject(); 45 | Assert.assertEquals("squid:S1161", reader.prop("rule")); 46 | Assert.assertEquals( 47 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/IssueResolverPage.java", 48 | reader.prop("component")); 49 | Assert.assertEquals(17, reader.propAsInt("line")); 50 | Assert.assertEquals(15, reader.propAsInt("offset")); 51 | Assert.assertEquals("wontfix", reader.prop("resolution")); 52 | Assert.assertEquals(Collections.emptyList(), reader.propValues("comments")); 53 | Assert.assertFalse(reader.hasNext()); 54 | reader.endObject(); 55 | 56 | Assert.assertFalse(reader.hasNext()); 57 | reader.endArray(); 58 | Assert.assertFalse(reader.hasNext()); 59 | Assert.assertFalse(reader.isEndOfDocument()); 60 | reader.endObject(); 61 | Assert.assertTrue(reader.isEndOfDocument()); 62 | reader.close(); 63 | } 64 | } 65 | 66 | @Test(expected = IllegalStateException.class) 67 | public void testInvalidName() throws IOException { 68 | try (InputStream inputStream = new ByteArrayInputStream(JSON.getBytes("UTF-8")); 69 | JsonReader reader = new JsonReader(inputStream)) { 70 | reader.beginObject(); 71 | reader.assertName("notVersion"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/page/IssueResolverPageTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.page; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.sonar.api.web.page.Context; 6 | 7 | public class IssueResolverPageTest { 8 | 9 | @Test 10 | public void test() { 11 | final IssueResolverPage subject = new IssueResolverPage(); 12 | final Context context = new Context(); 13 | 14 | Assert.assertEquals(0, context.getPages().size()); 15 | subject.define(context); 16 | Assert.assertEquals(1, context.getPages().size()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/ExportActionTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.sonarqube.ws.Common; 10 | import org.sonarqube.ws.Issues; 11 | 12 | public class ExportActionTest { 13 | 14 | @Test 15 | public void test() throws IOException { 16 | // Request 17 | final MockRequest request = new MockRequest(); 18 | request.setParam("projectKey", "my-project-key"); 19 | 20 | // Local call (first page) 21 | final Map localRequestParamsToCheckPageOne = new HashMap<>(); 22 | localRequestParamsToCheckPageOne.put("projectKeys", "my-project-key"); 23 | localRequestParamsToCheckPageOne.put("additionalFields", "comments"); 24 | localRequestParamsToCheckPageOne.put("statuses", "CONFIRMED,REOPENED,RESOLVED"); 25 | localRequestParamsToCheckPageOne.put("p", "1"); 26 | localRequestParamsToCheckPageOne.put("ps", "100"); 27 | 28 | final Issues.SearchWsResponse.Builder localRequestResponsePageOne = Issues.SearchWsResponse.newBuilder(); 29 | localRequestResponsePageOne.setPaging(Common.Paging.newBuilder().setTotal(2).setPageIndex(1).setPageSize(1)); 30 | localRequestResponsePageOne 31 | .addIssues(Issues.Issue.newBuilder().setKey("AVrdUwSCGyMCMhQpQjBw").setRule("xml:IllegalTabCheck") 32 | .setComponent("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:pom.xml") 33 | .setTextRange(Common.TextRange.newBuilder().setStartLine(4).setStartOffset(0)) 34 | .setResolution("FALSE-POSITIVE").setStatus("RESOLVED")); 35 | localRequestResponsePageOne.addComponents(Issues.Component.newBuilder() 36 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:pom.xml").setLongName("pom.xml")); 37 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageOne, 38 | localRequestResponsePageOne.build().toByteArray()); 39 | 40 | // Local call (second page) 41 | final Map localRequestParamsToCheckPageTwo = new HashMap<>(); 42 | localRequestParamsToCheckPageTwo.put("projectKeys", "my-project-key"); 43 | localRequestParamsToCheckPageTwo.put("additionalFields", "comments"); 44 | localRequestParamsToCheckPageTwo.put("statuses", "CONFIRMED,REOPENED,RESOLVED"); 45 | localRequestParamsToCheckPageTwo.put("p", "2"); 46 | localRequestParamsToCheckPageTwo.put("ps", "1"); 47 | 48 | final Issues.SearchWsResponse.Builder localRequestResponsePageTwo = Issues.SearchWsResponse.newBuilder(); 49 | localRequestResponsePageTwo.setPaging(Common.Paging.newBuilder().setTotal(2).setPageIndex(2).setPageSize(1)); 50 | localRequestResponsePageTwo 51 | .addIssues(Issues.Issue.newBuilder().setKey("AVrdUwS9GyMCMhQpQjBx").setRule("squid:S3776") 52 | .setComponent( 53 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 54 | .setTextRange(Common.TextRange.newBuilder().setStartLine(64).setStartOffset(16)) 55 | .setResolution("WONTFIX").setStatus("RESOLVED")); 56 | localRequestResponsePageTwo.addComponents(Issues.Component.newBuilder() 57 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 58 | .setLongName("src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java")); 59 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageTwo, 60 | localRequestResponsePageTwo.build().toByteArray()); 61 | 62 | // Response 63 | final MockResponse response = new MockResponse(); 64 | 65 | // Execute 66 | final ExportAction subject = new ExportAction(); 67 | subject.handle(request, response); 68 | 69 | // Validate 70 | final String result = new String(response.result(), "UTF-8"); 71 | Assert.assertEquals( 72 | "{\"version\":1,\"issues\":[" 73 | + "{\"longName\":\"pom.xml\",\"rule\":\"xml:IllegalTabCheck\",\"line\":4,\"status\":\"RESOLVED\",\"resolution\":\"FALSE-POSITIVE\",\"assignee\":\"\",\"comments\":[]}," 74 | + "{\"longName\":\"src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java\",\"rule\":\"squid:S3776\",\"line\":64,\"status\":\"RESOLVED\",\"resolution\":\"WONTFIX\",\"assignee\":\"\",\"comments\":[]}" 75 | + "]}", 76 | result); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/ImportActionTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.sonar.api.internal.apachecommons.io.IOUtils; 11 | import org.sonarqube.ws.Common; 12 | import org.sonarqube.ws.Issues; 13 | 14 | public class ImportActionTest { 15 | 16 | @Test 17 | public void test() throws IOException { 18 | // Request 19 | final MockRequest request = new MockRequest(); 20 | request.setParam("projectKey", "my-project-key"); 21 | request.setParam("preview", "false"); 22 | request.setParam("skipAssign", "false"); 23 | request.setParam("skipComments", "false"); 24 | request.setPart("data", 25 | new ByteArrayInputStream(removeInvalidJsonComments( 26 | IOUtils.toString(ImportActionTest.class.getResourceAsStream("ImportActionTest-request.json"))) 27 | .getBytes("UTF-8")), 28 | "resolved-issues.json"); 29 | 30 | // Local call - Search - First page 31 | final Map localRequestParamsToCheckPageOne = new HashMap<>(); 32 | localRequestParamsToCheckPageOne.put("projectKeys", "my-project-key"); 33 | localRequestParamsToCheckPageOne.put("additionalFields", "comments"); 34 | localRequestParamsToCheckPageOne.put("p", "1"); 35 | localRequestParamsToCheckPageOne.put("ps", "100"); 36 | 37 | final Issues.SearchWsResponse.Builder localRequestResponsePageOne = Issues.SearchWsResponse.newBuilder(); 38 | localRequestResponsePageOne.setPaging(Common.Paging.newBuilder().setTotal(9).setPageIndex(1).setPageSize(6)); 39 | // MATCHED ISSUE (NO ACTION) 40 | localRequestResponsePageOne 41 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey4").setRule("xml:IllegalTabCheck") 42 | .setComponent("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 43 | .setTextRange(Common.TextRange.newBuilder().setStartLine(4).setStartOffset(0)) 44 | .setResolution("FALSE-POSITIVE").setStatus("RESOLVED")); 45 | // UNMATCHED ISSUE 46 | localRequestResponsePageOne.addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey14") 47 | .setRule("xml:IllegalTabCheck") 48 | .setComponent("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 49 | .setTextRange(Common.TextRange.newBuilder().setStartLine(14).setStartOffset(0)).setStatus("OPEN")); 50 | // MATCHED ISSUE (CONFIRM; NO ASSIGN) 51 | localRequestResponsePageOne 52 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey55").setRule("squid:S3776") 53 | .setComponent( 54 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 55 | .setTextRange(Common.TextRange.newBuilder().setStartLine(55).setStartOffset(16)) 56 | .setAssignee("admin").setStatus("OPEN")); 57 | // MATCHED ISSUE (UNCONFIRM) 58 | localRequestResponsePageOne 59 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey56").setRule("squid:S3776") 60 | .setComponent( 61 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 62 | .setTextRange(Common.TextRange.newBuilder().setStartLine(56).setStartOffset(16)) 63 | .setAssignee("admin").setStatus("CONFIRMED")); 64 | // MATCHED ISSUE (REOPEN) 65 | localRequestResponsePageOne.addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey57") 66 | .setRule("squid:S3776") 67 | .setComponent( 68 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 69 | .setTextRange(Common.TextRange.newBuilder().setStartLine(57).setStartOffset(16)).setStatus("RESOLVED")); 70 | // MATCHED ISSUE (RESOLVE FIXED) 71 | localRequestResponsePageOne.addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey58") 72 | .setRule("squid:S3776") 73 | .setComponent( 74 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 75 | .setTextRange(Common.TextRange.newBuilder().setStartLine(58).setStartOffset(16)).setStatus("OPEN")); 76 | localRequestResponsePageOne.addComponents(Issues.Component.newBuilder() 77 | .setKey("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 78 | .setLongName("pom.xml")); 79 | localRequestResponsePageOne.addComponents(Issues.Component.newBuilder() 80 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 81 | .setLongName("src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java")); 82 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageOne, 83 | localRequestResponsePageOne.build().toByteArray()); 84 | 85 | // Local call - Search - Second page 86 | final Map localRequestParamsToCheckPageTwo = new HashMap<>(); 87 | localRequestParamsToCheckPageTwo.put("projectKeys", "my-project-key"); 88 | localRequestParamsToCheckPageTwo.put("additionalFields", "comments"); 89 | localRequestParamsToCheckPageTwo.put("p", "2"); 90 | localRequestParamsToCheckPageTwo.put("ps", "6"); 91 | 92 | final Issues.SearchWsResponse.Builder localRequestResponsePageTwo = Issues.SearchWsResponse.newBuilder(); 93 | localRequestResponsePageTwo.setPaging(Common.Paging.newBuilder().setTotal(9).setPageIndex(2).setPageSize(6)); 94 | // MATCHED ISSUE (RESOLVE FALSE-POSITIVE) 95 | localRequestResponsePageTwo.addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey59") 96 | .setRule("squid:S3776") 97 | .setComponent( 98 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 99 | .setTextRange(Common.TextRange.newBuilder().setStartLine(59).setStartOffset(16)).setStatus("REOPENED")); 100 | // MATCHED ISSUE (RESOLVE WONTFIX; ADD COMMENT) 101 | localRequestResponsePageTwo 102 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey60").setRule("squid:S3776") 103 | .setComponent( 104 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 105 | .setTextRange(Common.TextRange.newBuilder().setStartLine(60).setStartOffset(16)) 106 | .setComments(Issues.Comments.newBuilder() 107 | .addComments(Issues.Comment.newBuilder().setMarkdown("Comment one"))) 108 | .setStatus("CONFIRMED")); 109 | // MATCHED ISSUE (MATCH FAILURE) 110 | localRequestResponsePageTwo 111 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey61").setRule("squid:S3776") 112 | .setComponent( 113 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 114 | .setTextRange(Common.TextRange.newBuilder().setStartLine(61).setStartOffset(16)) 115 | .setStatus("RESOLVED").setResolution("WONTFIX")); 116 | 117 | localRequestResponsePageTwo.addComponents(Issues.Component.newBuilder() 118 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 119 | .setLongName("src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java")); 120 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageTwo, 121 | localRequestResponsePageTwo.build().toByteArray()); 122 | 123 | // Local call - MATCHED ISSUE (NO ACTION; ASSIGN) - Assign 124 | final Map localRequestParamsAssign4 = new HashMap<>(); 125 | localRequestParamsAssign4.put("issue", "TotaleAndereKey4"); 126 | localRequestParamsAssign4.put("assignee", "admin"); 127 | 128 | request.mockLocalRequest("api/issues/assign", localRequestParamsAssign4, 129 | Issues.Operation.newBuilder().build().toByteArray()); 130 | 131 | // Local call - MATCHED ISSUE (CONFIRM; NO ASSIGN) 132 | final Map localRequestParamsTransition55 = new HashMap<>(); 133 | localRequestParamsTransition55.put("issue", "TotaleAndereKey55"); 134 | localRequestParamsTransition55.put("transition", "confirm"); 135 | 136 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition55, 137 | Issues.Operation.newBuilder().build().toByteArray()); 138 | 139 | // Local call - MATCHED ISSUE (UNCONFIRM; REASSIGN) 140 | final Map localRequestParamsTransition56 = new HashMap<>(); 141 | localRequestParamsTransition56.put("issue", "TotaleAndereKey56"); 142 | localRequestParamsTransition56.put("transition", "unconfirm"); 143 | 144 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition56, 145 | Issues.Operation.newBuilder().build().toByteArray()); 146 | 147 | final Map localRequestParamsAssign56 = new HashMap<>(); 148 | localRequestParamsAssign56.put("issue", "TotaleAndereKey56"); 149 | localRequestParamsAssign56.put("assignee", "unknown"); 150 | 151 | request.mockLocalRequest("api/issues/assign", localRequestParamsAssign56, 400, 152 | Issues.Operation.newBuilder().build().toByteArray()); 153 | 154 | // Local call - MATCHED ISSUE (REOPEN) 155 | final Map localRequestParamsTransition57 = new HashMap<>(); 156 | localRequestParamsTransition57.put("issue", "TotaleAndereKey57"); 157 | localRequestParamsTransition57.put("transition", "reopen"); 158 | 159 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition57, 400, 160 | Issues.Operation.newBuilder().build().toByteArray()); 161 | 162 | // Local call - MATCHED ISSUE (RESOLVE FIXED) 163 | final Map localRequestParamsTransition58 = new HashMap<>(); 164 | localRequestParamsTransition58.put("issue", "TotaleAndereKey58"); 165 | localRequestParamsTransition58.put("transition", "resolve"); 166 | 167 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition58, 168 | Issues.Operation.newBuilder().build().toByteArray()); 169 | 170 | // Local call - MATCHED ISSUE (RESOLVE FALSE-POSITIVE) 171 | final Map localRequestParamsTransition59 = new HashMap<>(); 172 | localRequestParamsTransition59.put("issue", "TotaleAndereKey59"); 173 | localRequestParamsTransition59.put("transition", "falsepositive"); 174 | 175 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition59, 176 | Issues.Operation.newBuilder().build().toByteArray()); 177 | 178 | // Local call - MATCHED ISSUE (RESOLVE WONTFIX; ADD COMMENT) 179 | final Map localRequestParamsTransition60 = new HashMap<>(); 180 | localRequestParamsTransition60.put("issue", "TotaleAndereKey60"); 181 | localRequestParamsTransition60.put("transition", "wontfix"); 182 | 183 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsTransition60, 184 | Issues.Operation.newBuilder().build().toByteArray()); 185 | 186 | final Map localRequestParamsAddComment60a = new HashMap<>(); 187 | localRequestParamsAddComment60a.put("issue", "TotaleAndereKey60"); 188 | localRequestParamsAddComment60a.put("text", "Comment two"); 189 | 190 | request.mockLocalRequest("api/issues/add_comment", localRequestParamsAddComment60a, 191 | Issues.Operation.newBuilder().build().toByteArray()); 192 | 193 | final Map localRequestParamsAddComment60b = new HashMap<>(); 194 | localRequestParamsAddComment60b.put("issue", "TotaleAndereKey60"); 195 | localRequestParamsAddComment60b.put("text", "Comment three"); 196 | 197 | request.mockLocalRequest("api/issues/add_comment", localRequestParamsAddComment60b, 400, 198 | Issues.Operation.newBuilder().build().toByteArray()); 199 | 200 | // Response 201 | final MockResponse response = new MockResponse(); 202 | 203 | // Execute 204 | final ImportAction subject = new ImportAction(); 205 | subject.handle(request, response); 206 | 207 | request.validateNoMoreLocalRequests(); 208 | 209 | // Validate 210 | final String result = new String(response.result(), "UTF-8"); 211 | Assert.assertEquals( 212 | "{\"preview\":false,\"issues\":10,\"duplicateKeys\":1," 213 | + "\"matchedIssues\":8,\"matchFailures\":[\"Could not determine transition for issue with key 'TotaleAndereKey61'; current status is 'RESOLVED' and resolution is 'WONTFIX'; wanted status is 'RESOLVED' and resolution is 'FALSE-POSITIVE'\"]," 214 | + "\"transitionedIssues\":6,\"transitionFailures\":[\"Could not transition issue with key 'TotaleAndereKey57' using transition 'reopen'\"]," 215 | + "\"assignedIssues\":2,\"assignFailures\":[\"Could not assign issue with key 'TotaleAndereKey56' to user 'unknown'\"]," 216 | + "\"commentedIssues\":1,\"commentFailures\":[\"Could not add comment to issue with key 'TotaleAndereKey60'\"]}", 217 | result); 218 | } 219 | 220 | private String removeInvalidJsonComments(String json) { 221 | String result = json.replaceAll("(?m)//.*$", ""); 222 | System.out.println(result); 223 | return result; 224 | } 225 | 226 | @Test(expected=IllegalStateException.class) 227 | public void invalidData() throws IOException { 228 | final MockRequest request = new MockRequest(); 229 | request.setParam("projectKey", "my-project-key"); 230 | request.setParam("preview", "false"); 231 | request.setPart("data", new ByteArrayInputStream("{\"version\":1}".getBytes("UTF-8")), "resolved-issues.json"); 232 | 233 | // Response 234 | final MockResponse response = new MockResponse(); 235 | 236 | // Execute 237 | final ImportAction subject = new ImportAction(); 238 | subject.handle(request, response); 239 | } 240 | 241 | @Test(expected=IllegalStateException.class) 242 | public void invalidVersion() throws IOException { 243 | final MockRequest request = new MockRequest(); 244 | request.setParam("projectKey", "my-project-key"); 245 | request.setParam("preview", "false"); 246 | request.setPart("data", new ByteArrayInputStream("{\"version\":0}".getBytes("UTF-8")), "resolved-issues.json"); 247 | 248 | // Response 249 | final MockResponse response = new MockResponse(); 250 | 251 | // Execute 252 | final ImportAction subject = new ImportAction(); 253 | subject.handle(request, response); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/IssueResolverWebServiceTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.sonar.api.server.ws.WebService; 6 | import org.sonar.api.server.ws.WebService.Action; 7 | import org.sonar.api.server.ws.WebService.Controller; 8 | 9 | public class IssueResolverWebServiceTest { 10 | 11 | @Test 12 | public void test() { 13 | final IssueResolverWebService subject = new IssueResolverWebService(new ExportAction(), new ImportAction(), 14 | new UpdateAction()); 15 | 16 | final WebService.Context context = new WebService.Context(); 17 | Assert.assertEquals(0, context.controllers().size()); 18 | subject.define(context); 19 | Assert.assertEquals(1, context.controllers().size()); 20 | 21 | // Controller 22 | Controller controller = context.controller("api/issueresolver"); 23 | Assert.assertNotNull(controller); 24 | Assert.assertEquals(3, controller.actions().size()); 25 | 26 | // Export 27 | Action exportAction = controller.action("export"); 28 | Assert.assertNotNull(exportAction); 29 | Assert.assertTrue(exportAction.handler() instanceof ExportAction); 30 | Assert.assertEquals(1, exportAction.params().size()); 31 | Assert.assertNotNull(exportAction.param("projectKey")); 32 | 33 | // Import 34 | Action importAction = controller.action("import"); 35 | Assert.assertNotNull(importAction); 36 | Assert.assertTrue(importAction.handler() instanceof ImportAction); 37 | Assert.assertEquals(5, importAction.params().size()); 38 | Assert.assertNotNull(importAction.param("projectKey")); 39 | Assert.assertNotNull(importAction.param("preview")); 40 | Assert.assertNotNull(importAction.param("data")); 41 | Assert.assertNotNull(importAction.param("skipAssign")); 42 | Assert.assertNotNull(importAction.param("skipComments")); 43 | 44 | // Update 45 | Action updateAction = controller.action("update"); 46 | Assert.assertNotNull(updateAction); 47 | Assert.assertTrue(updateAction.handler() instanceof UpdateAction); 48 | Assert.assertEquals(5, updateAction.params().size()); 49 | Assert.assertNotNull(updateAction.param("fromProjectKey")); 50 | Assert.assertNotNull(updateAction.param("projectKey")); 51 | Assert.assertNotNull(updateAction.param("preview")); 52 | Assert.assertNotNull(updateAction.param("skipAssign")); 53 | Assert.assertNotNull(updateAction.param("skipComments")); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/MockRequest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static java.util.Collections.emptyList; 5 | import static java.util.Collections.singletonList; 6 | 7 | import java.io.InputStream; 8 | import java.io.UnsupportedEncodingException; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import javax.annotation.Nullable; 16 | 17 | import org.apache.commons.io.IOUtils; 18 | import org.junit.Assert; 19 | import org.mockito.Mockito; 20 | import org.mockito.invocation.InvocationOnMock; 21 | import org.mockito.stubbing.Answer; 22 | import org.sonar.api.server.ws.LocalConnector; 23 | import org.sonar.api.server.ws.LocalConnector.LocalRequest; 24 | import org.sonar.api.server.ws.LocalConnector.LocalResponse; 25 | import org.sonar.api.server.ws.Request; 26 | import org.sonar.api.server.ws.internal.PartImpl; 27 | 28 | import com.google.common.collect.Maps; 29 | 30 | public class MockRequest extends Request { 31 | 32 | private final Map params = Maps.newHashMap(); 33 | private final Map parts = Maps.newHashMap(); 34 | private String method = "GET"; 35 | private String mediaType = "application/json"; 36 | private String path; 37 | 38 | private LocalConnector localConnectorMock = Mockito.mock(LocalConnector.class, new LocalRequestAnswer()); 39 | 40 | /* ************************************ */ 41 | /* *** INPUT ************************** */ 42 | /* ************************************ */ 43 | 44 | public void setMethod(String method) { 45 | checkNotNull(method); 46 | this.method = method; 47 | } 48 | 49 | public void setMediaType(String mediaType) { 50 | checkNotNull(mediaType); 51 | this.mediaType = mediaType; 52 | } 53 | 54 | public void setParam(String key, @Nullable String value) { 55 | if (value == null) { 56 | params.remove(key); 57 | } else { 58 | params.put(key, value); 59 | } 60 | } 61 | 62 | public void setPart(String key, InputStream input, String fileName) { 63 | parts.put(key, new PartImpl(input, fileName)); 64 | } 65 | 66 | public void setPath(String path) { 67 | this.path = path; 68 | } 69 | 70 | /* ************************************ */ 71 | /* *** RESPONSE *********************** */ 72 | /* ************************************ */ 73 | 74 | @Override 75 | public String method() { 76 | return method; 77 | } 78 | 79 | @Override 80 | public String getMediaType() { 81 | return mediaType; 82 | } 83 | 84 | @Override 85 | public boolean hasParam(String key) { 86 | return params.keySet().contains(key); 87 | } 88 | 89 | @Override 90 | public String param(String key) { 91 | return params.get(key); 92 | } 93 | 94 | @Override 95 | public List multiParam(String key) { 96 | String value = params.get(key); 97 | return value == null ? emptyList() : singletonList(value); 98 | } 99 | 100 | @Override 101 | public InputStream paramAsInputStream(String key) { 102 | return IOUtils.toInputStream(param(key)); 103 | } 104 | 105 | @Override 106 | public Part paramAsPart(String key) { 107 | return parts.get(key); 108 | } 109 | 110 | /** 111 | * Returns the {@link Mockito} mock used for the local connector. 112 | * 113 | * @return local connector mock 114 | */ 115 | @Override 116 | public LocalConnector localConnector() { 117 | return localConnectorMock; 118 | } 119 | 120 | @Override 121 | public String getPath() { 122 | return path; 123 | } 124 | 125 | /* ************************************ */ 126 | /* *** RESPONSE *********************** */ 127 | /* ************************************ */ 128 | 129 | private Map> localRequests = new HashMap<>(); 130 | 131 | public void mockLocalRequest(String path, Map paramsToCheck, byte[] resultToSend) { 132 | mockLocalRequest(path, paramsToCheck, 200, resultToSend); 133 | } 134 | 135 | public void mockLocalRequest(String path, Map paramsToCheck, int status, byte[] resultToSend) { 136 | if (!localRequests.containsKey(path)) { 137 | localRequests.put(path, new ArrayList()); 138 | } 139 | 140 | localRequests.get(path).add(new LocalRequestData(paramsToCheck,status, resultToSend)); 141 | } 142 | public void validateNoMoreLocalRequests() { 143 | for(Map.Entry> localRequest : localRequests.entrySet()) { 144 | Assert.assertTrue("Not all requests for " + localRequest.getKey() + " have been called" , localRequest.getValue().isEmpty()); 145 | } 146 | } 147 | 148 | private final class LocalRequestAnswer implements Answer { 149 | 150 | @Override 151 | public LocalResponse answer(InvocationOnMock invocation) throws Throwable { 152 | LocalRequest request = invocation.getArgument(0); 153 | 154 | if (!localRequests.containsKey(request.getPath())) { 155 | throw new IllegalStateException("No local request for '" + request.getPath() + "' expected"); 156 | } 157 | 158 | if(localRequests.get(request.getPath()).isEmpty()) { 159 | throw new IllegalStateException("No more local requests for '" + request.getPath() + "' expected"); 160 | } 161 | 162 | final LocalRequestData requestData = localRequests.get(request.getPath()).remove(0); 163 | for (Map.Entry param : requestData.getParamsToCheck().entrySet()) { 164 | Assert.assertEquals("Local request parameter different", param.getValue(), 165 | request.getParam(param.getKey())); 166 | } 167 | 168 | return requestData; 169 | } 170 | } 171 | 172 | private final class LocalRequestData implements LocalResponse { 173 | private final Map paramsToCheck; 174 | private final int status; 175 | private final byte[] resultToSend; 176 | 177 | public LocalRequestData(Map paramsToCheck, int status, byte[] resultToSend) { 178 | super(); 179 | this.paramsToCheck = paramsToCheck; 180 | this.status = status; 181 | this.resultToSend = resultToSend; 182 | } 183 | 184 | public Map getParamsToCheck() { 185 | return paramsToCheck; 186 | } 187 | 188 | @Override 189 | public int getStatus() { 190 | return status; 191 | } 192 | 193 | @Override 194 | public String getMediaType() { 195 | return "application/json"; 196 | } 197 | 198 | @Override 199 | public byte[] getBytes() { 200 | return resultToSend; 201 | } 202 | 203 | 204 | @Override 205 | public Collection getHeaderNames() { 206 | throw new UnsupportedOperationException(); 207 | } 208 | 209 | @Override 210 | public String getHeader(String name) { 211 | throw new UnsupportedOperationException(); 212 | } 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/MockResponse.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.OutputStream; 5 | import java.io.OutputStreamWriter; 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import org.mockito.Mockito; 11 | import org.sonar.api.server.ws.LocalConnector; 12 | import org.sonar.api.server.ws.Response; 13 | import org.sonar.api.utils.text.JsonWriter; 14 | import org.sonar.api.utils.text.XmlWriter; 15 | 16 | public class MockResponse implements Response, Response.Stream { 17 | 18 | private Map headers = new HashMap<>(); 19 | private String mediaType; 20 | private int status; 21 | private ByteArrayOutputStream baos = new ByteArrayOutputStream(); 22 | 23 | /* ************************************ */ 24 | /* *** RESPONSE *********************** */ 25 | /* ************************************ */ 26 | 27 | @Override 28 | public JsonWriter newJsonWriter() { 29 | return JsonWriter.of(new OutputStreamWriter(output())); 30 | } 31 | 32 | @Override 33 | public XmlWriter newXmlWriter() { 34 | throw new UnsupportedOperationException(); 35 | } 36 | 37 | @Override 38 | public Response noContent() { 39 | throw new UnsupportedOperationException(); 40 | } 41 | 42 | @Override 43 | public Response setHeader(String name, String value) { 44 | headers.put(name, value); 45 | return this; 46 | } 47 | 48 | @Override 49 | public Collection getHeaderNames() { 50 | return headers.keySet(); 51 | } 52 | 53 | @Override 54 | public String getHeader(String name) { 55 | return headers.get(name); 56 | } 57 | 58 | @Override 59 | public Stream stream() { 60 | return this; 61 | } 62 | 63 | /* ************************************ */ 64 | /* *** STREAM ************************* */ 65 | /* ************************************ */ 66 | 67 | @Override 68 | public Stream setMediaType(String s) { 69 | mediaType = s; 70 | return this; 71 | } 72 | 73 | @Override 74 | public Stream setStatus(int httpStatus) { 75 | status = httpStatus; 76 | return this; 77 | } 78 | 79 | @Override 80 | public OutputStream output() { 81 | return baos; 82 | } 83 | 84 | /* ************************************ */ 85 | /* *** OUTPUT ************************* */ 86 | /* ************************************ */ 87 | 88 | public byte[] result() { 89 | return baos.toByteArray(); 90 | } 91 | 92 | public Map getHeaders() { 93 | return headers; 94 | } 95 | 96 | public String getMediaType() { 97 | return mediaType; 98 | } 99 | 100 | public int getStatus() { 101 | return status; 102 | } 103 | 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/nl/futureedge/sonar/plugin/issueresolver/ws/UpdateActionTest.java: -------------------------------------------------------------------------------- 1 | package nl.futureedge.sonar.plugin.issueresolver.ws; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.sonarqube.ws.Common; 10 | import org.sonarqube.ws.Issues; 11 | 12 | public class UpdateActionTest { 13 | 14 | @Test 15 | public void test() throws IOException { 16 | // Request 17 | final MockRequest request = new MockRequest(); 18 | request.setParam("fromProjectKey", "base-project-key"); 19 | request.setParam("projectKey", "my-project-key"); 20 | request.setParam("preview", "false"); 21 | request.setParam("skipAssign", "false"); 22 | request.setParam("skipComments", "false"); 23 | 24 | // Local call (first page) 25 | final Map localRequestBaseParamsToCheckPageOne = new HashMap<>(); 26 | localRequestBaseParamsToCheckPageOne.put("projectKeys", "base-project-key"); 27 | localRequestBaseParamsToCheckPageOne.put("additionalFields", "comments"); 28 | localRequestBaseParamsToCheckPageOne.put("statuses", "CONFIRMED,REOPENED,RESOLVED"); 29 | localRequestBaseParamsToCheckPageOne.put("p", "1"); 30 | localRequestBaseParamsToCheckPageOne.put("ps", "100"); 31 | 32 | final Issues.SearchWsResponse.Builder localRequestBaseResponsePageOne = Issues.SearchWsResponse.newBuilder(); 33 | localRequestBaseResponsePageOne 34 | .setPaging(Common.Paging.newBuilder().setTotal(3).setPageIndex(1).setPageSize(2)); 35 | localRequestBaseResponsePageOne 36 | .addIssues(Issues.Issue.newBuilder().setKey("AVrdUwSCGyMCMhQpQjBw").setRule("xml:IllegalTabCheck") 37 | .setComponent("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:pom.xml") 38 | .setTextRange(Common.TextRange.newBuilder().setStartLine(4).setStartOffset(0)) 39 | .setResolution("FALSE-POSITIVE").setStatus("RESOLVED")); 40 | localRequestBaseResponsePageOne 41 | .addIssues(Issues.Issue.newBuilder().setKey("AVrdUwSCGyMCMhQpQjBq").setRule("xml:IllegalTabCheck") 42 | .setComponent("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:pom.xml") 43 | .setTextRange(Common.TextRange.newBuilder().setStartLine(7).setStartOffset(0)) 44 | .setResolution("FALSE-POSITIVE").setStatus("RESOLVED")); 45 | localRequestBaseResponsePageOne.addComponents(Issues.Component.newBuilder() 46 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:pom.xml").setLongName("pom.xml")); 47 | request.mockLocalRequest("api/issues/search", localRequestBaseParamsToCheckPageOne, 48 | localRequestBaseResponsePageOne.build().toByteArray()); 49 | 50 | // Local call (second page) 51 | final Map localRequestBaseParamsToCheckPageTwo = new HashMap<>(); 52 | localRequestBaseParamsToCheckPageTwo.put("projectKeys", "base-project-key"); 53 | localRequestBaseParamsToCheckPageTwo.put("additionalFields", "comments"); 54 | localRequestBaseParamsToCheckPageTwo.put("statuses", "CONFIRMED,REOPENED,RESOLVED"); 55 | localRequestBaseParamsToCheckPageTwo.put("p", "2"); 56 | localRequestBaseParamsToCheckPageTwo.put("ps", "2"); 57 | 58 | final Issues.SearchWsResponse.Builder localRequestBaseResponsePageTwo = Issues.SearchWsResponse.newBuilder(); 59 | localRequestBaseResponsePageTwo 60 | .setPaging(Common.Paging.newBuilder().setTotal(3).setPageIndex(2).setPageSize(2)); 61 | localRequestBaseResponsePageTwo 62 | .addIssues(Issues.Issue.newBuilder().setKey("AVrdUwS9GyMCMhQpQjBx").setRule("squid:S3776") 63 | .setComponent( 64 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 65 | .setTextRange(Common.TextRange.newBuilder().setStartLine(64).setStartOffset(16)) 66 | .setResolution("WONTFIX").setStatus("RESOLVED")); 67 | localRequestBaseResponsePageTwo.addComponents(Issues.Component.newBuilder() 68 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 69 | .setLongName("src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java")); 70 | request.mockLocalRequest("api/issues/search", localRequestBaseParamsToCheckPageTwo, 71 | localRequestBaseResponsePageTwo.build().toByteArray()); 72 | 73 | // Local call (first page) 74 | final Map localRequestParamsToCheckPageOne = new HashMap<>(); 75 | localRequestParamsToCheckPageOne.put("projectKeys", "my-project-key"); 76 | localRequestParamsToCheckPageOne.put("additionalFields", "comments"); 77 | localRequestParamsToCheckPageOne.put("p", "1"); 78 | localRequestParamsToCheckPageOne.put("ps", "100"); 79 | 80 | final Issues.SearchWsResponse.Builder localRequestResponsePageOne = Issues.SearchWsResponse.newBuilder(); 81 | localRequestResponsePageOne.setPaging(Common.Paging.newBuilder().setTotal(3).setPageIndex(1).setPageSize(2)); 82 | localRequestResponsePageOne 83 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey1").setRule("xml:IllegalTabCheck") 84 | .setComponent("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 85 | .setTextRange(Common.TextRange.newBuilder().setStartLine(4).setStartOffset(0)) 86 | .setResolution("FALSE-POSITIVE").setStatus("RESOLVED")); 87 | localRequestResponsePageOne.addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey1b") 88 | .setRule("xml:IllegalTabCheck") 89 | .setComponent("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 90 | .setTextRange(Common.TextRange.newBuilder().setStartLine(14).setStartOffset(0)).setStatus("OPEN")); 91 | localRequestResponsePageOne.addComponents(Issues.Component.newBuilder() 92 | .setKey("nl.future-edge.sonarqube.plugins:myBranch:sonar-issueresolver-plugin:pom.xml") 93 | .setLongName("pom.xml")); 94 | 95 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageOne, 96 | localRequestResponsePageOne.build().toByteArray()); 97 | 98 | // Local call (second page) 99 | final Map localRequestParamsToCheckPageTwo = new HashMap<>(); 100 | localRequestParamsToCheckPageTwo.put("projectKeys", "my-project-key"); 101 | localRequestParamsToCheckPageTwo.put("additionalFields", "comments"); 102 | localRequestParamsToCheckPageTwo.put("p", "2"); 103 | localRequestParamsToCheckPageTwo.put("ps", "2"); 104 | 105 | final Issues.SearchWsResponse.Builder localRequestResponsePageTwo = Issues.SearchWsResponse.newBuilder(); 106 | localRequestResponsePageTwo.setPaging(Common.Paging.newBuilder().setTotal(2).setPageIndex(2).setPageSize(1)); 107 | localRequestResponsePageTwo 108 | .addIssues(Issues.Issue.newBuilder().setKey("TotaleAndereKey2").setRule("squid:S3776") 109 | .setComponent( 110 | "nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 111 | .setTextRange(Common.TextRange.newBuilder().setStartLine(64).setStartOffset(16)) 112 | .setComments(Issues.Comments.newBuilder() 113 | .addComments(Issues.Comment.newBuilder().setMarkdown("Comment one"))) 114 | .setStatus("OPEN")); 115 | localRequestResponsePageTwo.addComponents(Issues.Component.newBuilder() 116 | .setKey("nl.future-edge.sonarqube.plugins:sonar-issueresolver-plugin:myBranch:src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java") 117 | .setLongName("src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java")); 118 | request.mockLocalRequest("api/issues/search", localRequestParamsToCheckPageTwo, 119 | localRequestResponsePageTwo.build().toByteArray()); 120 | 121 | // Local call (resolve (second) issue) 122 | final Map localRequestParamsResolveIssue = new HashMap<>(); 123 | localRequestParamsResolveIssue.put("issue", "TotaleAndereKey2"); 124 | localRequestParamsResolveIssue.put("transition", "wontfix"); 125 | 126 | Issues.Operation.Builder localRequestResponseResolveIssue = Issues.Operation.newBuilder(); 127 | request.mockLocalRequest("api/issues/do_transition", localRequestParamsResolveIssue, 128 | localRequestResponseResolveIssue.build().toByteArray()); 129 | 130 | // Local call (add (second) comment) 131 | final Map localRequestParamsAddComment = new HashMap<>(); 132 | localRequestParamsAddComment.put("issue", "TotaleAndereKey2"); 133 | localRequestParamsAddComment.put("text", "Comment two"); 134 | 135 | Issues.Operation.Builder localRequestResponseAddComment = Issues.Operation.newBuilder(); 136 | request.mockLocalRequest("api/issues/add_comment", localRequestParamsAddComment, 137 | localRequestResponseAddComment.build().toByteArray()); 138 | 139 | // Response 140 | final MockResponse response = new MockResponse(); 141 | 142 | // Execute 143 | final UpdateAction subject = new UpdateAction(); 144 | subject.handle(request, response); 145 | 146 | // Validate 147 | final String result = new String(response.result(), "UTF-8"); 148 | Assert.assertEquals("{\"preview\":false,\"issues\":3,\"duplicateKeys\":0," 149 | + "\"matchedIssues\":2,\"matchFailures\":[]," + "\"transitionedIssues\":1,\"transitionFailures\":[]," 150 | + "\"assignedIssues\":0,\"assignFailures\":[]," + "\"commentedIssues\":0,\"commentFailures\":[]}", 151 | result); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/nl/futureedge/sonar/plugin/issueresolver/ws/ImportActionTest-request.json: -------------------------------------------------------------------------------- 1 | { // Yes, comments are invalid in JSON, but the test filters them out 2 | "version": 1, 3 | "issues": [ 4 | { // MATCHED ISSUE (NO ACTION; ASSIGN) 5 | "longName": "pom.xml", 6 | "rule": "xml:IllegalTabCheck", 7 | "line": 4, 8 | "status": "RESOLVED", 9 | "resolution": "FALSE-POSITIVE", 10 | "assignee": "admin", 11 | "comments": [] 12 | }, 13 | { // DUPLICATE KEY 14 | "longName": "pom.xml", 15 | "rule": "xml:IllegalTabCheck", 16 | "line": 4, 17 | "status": "RESOLVED", 18 | "resolution": "FALSE-POSITIVE", 19 | "assignee": "", 20 | "comments": [] 21 | }, 22 | { // UNMATCHED ISSUE 23 | "longName": "pom.xml", 24 | "rule": "xml:IllegalTabCheck", 25 | "line": 7, 26 | "status": "RESOLVED", 27 | "resolution": "FALSE-POSITIVE", 28 | "assignee": "", 29 | "comments": [] 30 | }, 31 | { // MATCHED ISSUE (CONFIRM; NO ASSIGN) 32 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 33 | "rule": "squid:S3776", 34 | "line": 55, 35 | "status": "CONFIRMED", 36 | "resolution": "", 37 | "assignee": "admin", 38 | "comments": [] 39 | }, 40 | { // MATCHED ISSUE (UNCONFIRM; REASSIGN) 41 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 42 | "rule": "squid:S3776", 43 | "line": 56, 44 | "status": "REOPENED", 45 | "resolution": "", 46 | "assignee": "unknown", 47 | "comments": [] 48 | }, 49 | { // MATCHED ISSUE (REOPEN) 50 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 51 | "rule": "squid:S3776", 52 | "line": 57, 53 | "status": "REOPENED", 54 | "resolution": "", 55 | "assignee": "", 56 | "comments": [] 57 | }, 58 | { // MATCHED ISSUE (RESOLVE FIXED) 59 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 60 | "rule": "squid:S3776", 61 | "line": 58, 62 | "status": "RESOLVED", 63 | "resolution": "FIXED", 64 | "assignee": "", 65 | "comments": [] 66 | }, 67 | { // MATCHED ISSUE (RESOLVE FALSE-POSITIVE) 68 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 69 | "rule": "squid:S3776", 70 | "line": 59, 71 | "status": "RESOLVED", 72 | "resolution": "FALSE-POSITIVE", 73 | "assignee": "", 74 | "comments": [] 75 | }, 76 | { // MATCHED ISSUE (RESOLVE WONTFIX; ADD COMMENT) 77 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 78 | "rule": "squid:S3776", 79 | "line": 60, 80 | "status": "RESOLVED", 81 | "resolution": "WONTFIX", 82 | "assignee": "", 83 | "comments": ["Comment one","Comment two","Comment three"] 84 | }, 85 | { // MATCHED ISSUE (MATCH FAILURE) 86 | "longName": "src/main/java/nl/futureedge/sonar/plugin/issueresolver/issues/IssueKey.java", 87 | "rule": "squid:S3776", 88 | "line": 61, 89 | "status": "RESOLVED", 90 | "resolution": "FALSE-POSITIVE", 91 | "assignee": "", 92 | "comments": [] 93 | } 94 | ] 95 | } --------------------------------------------------------------------------------