├── .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 [](https://travis-ci.org/willemsrb/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