1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.changes.jira;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.StringWriter;
24 import java.net.URI;
25 import java.text.ParseException;
26 import java.text.SimpleDateFormat;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.Map;
33
34 import com.fasterxml.jackson.core.JsonFactory;
35 import com.fasterxml.jackson.core.JsonGenerator;
36 import com.fasterxml.jackson.core.JsonParser;
37 import com.fasterxml.jackson.databind.JsonNode;
38 import com.fasterxml.jackson.databind.MappingJsonFactory;
39 import org.apache.http.HttpHeaders;
40 import org.apache.http.HttpHost;
41 import org.apache.http.HttpResponse;
42 import org.apache.http.HttpStatus;
43 import org.apache.http.auth.AuthScope;
44 import org.apache.http.auth.UsernamePasswordCredentials;
45 import org.apache.http.client.CredentialsProvider;
46 import org.apache.http.client.config.RequestConfig;
47 import org.apache.http.client.methods.CloseableHttpResponse;
48 import org.apache.http.client.methods.HttpGet;
49 import org.apache.http.client.methods.HttpPost;
50 import org.apache.http.entity.ContentType;
51 import org.apache.http.entity.StringEntity;
52 import org.apache.http.impl.client.BasicCookieStore;
53 import org.apache.http.impl.client.BasicCredentialsProvider;
54 import org.apache.http.impl.client.CloseableHttpClient;
55 import org.apache.http.impl.client.HttpClientBuilder;
56 import org.apache.http.impl.client.HttpClients;
57 import org.apache.http.message.BasicHeader;
58 import org.apache.maven.plugin.MojoExecutionException;
59 import org.apache.maven.plugin.MojoFailureException;
60 import org.apache.maven.plugin.logging.Log;
61 import org.apache.maven.plugins.changes.issues.Issue;
62 import org.apache.maven.plugins.changes.issues.IssueUtils;
63 import org.apache.maven.project.MavenProject;
64 import org.apache.maven.settings.Proxy;
65 import org.apache.maven.settings.Server;
66 import org.apache.maven.settings.Settings;
67 import org.apache.maven.settings.building.SettingsProblem;
68 import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
69 import org.apache.maven.settings.crypto.SettingsDecrypter;
70 import org.apache.maven.settings.crypto.SettingsDecryptionResult;
71
72
73
74
75
76 public class RestJiraDownloader {
77
78
79 private Log log;
80
81
82 private MavenProject project;
83
84
85 private Settings settings;
86
87 private SettingsDecrypter settingsDecrypter;
88
89
90 private boolean onlyCurrentVersion;
91
92
93 protected String versionPrefix;
94
95
96 protected int nbEntriesMax;
97
98
99 protected String filter;
100
101
102 protected String fixVersionIds;
103
104
105 protected String statusIds;
106
107
108 protected String resolutionIds;
109
110
111 protected String priorityIds;
112
113
114 protected String component;
115
116
117 protected String typeIds;
118
119
120 protected String sortColumnNames;
121
122
123 protected String jiraUser;
124
125
126 protected String jiraPassword;
127
128 private String jiraServerId;
129
130
131 protected String jiraDatePattern;
132
133 protected int connectionTimeout;
134
135 protected int receiveTimout;
136
137 private List<Issue> issueList;
138
139 private JsonFactory jsonFactory;
140
141 private SimpleDateFormat dateFormat;
142
143 private List<String> resolvedFixVersionIds;
144
145 private List<String> resolvedStatusIds;
146
147 private List<String> resolvedComponentIds;
148
149 private List<String> resolvedTypeIds;
150
151 private List<String> resolvedResolutionIds;
152
153 private List<String> resolvedPriorityIds;
154
155
156
157
158
159
160 protected String getFixFor() {
161 if (onlyCurrentVersion) {
162
163
164
165 String version = (versionPrefix == null ? "" : versionPrefix) + project.getVersion();
166
167
168 if (version.endsWith(IssueUtils.SNAPSHOT_SUFFIX)) {
169 return version.substring(0, version.length() - IssueUtils.SNAPSHOT_SUFFIX.length());
170 } else {
171 return version;
172 }
173 } else {
174 return null;
175 }
176 }
177
178
179
180
181
182
183 public void setMavenProject(MavenProject thisProject) {
184 this.project = thisProject;
185 }
186
187 public void setLog(Log log) {
188 this.log = log;
189 }
190
191 protected Log getLog() {
192 return log;
193 }
194
195 public void setSettings(Settings settings) {
196 this.settings = settings;
197 }
198
199 public void setSettingsDecrypter(SettingsDecrypter settingsDecrypter) {
200 this.settingsDecrypter = settingsDecrypter;
201 }
202
203 public void setOnlyCurrentVersion(boolean onlyCurrentVersion) {
204 this.onlyCurrentVersion = onlyCurrentVersion;
205 }
206
207 public void setVersionPrefix(String versionPrefix) {
208 this.versionPrefix = versionPrefix;
209 }
210
211 public void setJiraDatePattern(String jiraDatePattern) {
212 this.jiraDatePattern = jiraDatePattern;
213 }
214
215
216
217
218
219
220 public void setNbEntries(final int nbEntries) {
221 nbEntriesMax = nbEntries;
222 }
223
224
225
226
227
228
229 public void setStatusIds(String thisStatusIds) {
230 statusIds = thisStatusIds;
231 }
232
233
234
235
236
237
238 public void setPriorityIds(String thisPriorityIds) {
239 priorityIds = thisPriorityIds;
240 }
241
242
243
244
245
246
247 public void setResolutionIds(String thisResolutionIds) {
248 resolutionIds = thisResolutionIds;
249 }
250
251
252
253
254
255
256 public void setSortColumnNames(String thisSortColumnNames) {
257 sortColumnNames = thisSortColumnNames;
258 }
259
260
261
262
263
264
265 public void setJiraPassword(final String thisJiraPassword) {
266 this.jiraPassword = thisJiraPassword;
267 }
268
269
270
271
272
273
274 public void setJiraUser(String thisJiraUser) {
275 this.jiraUser = thisJiraUser;
276 }
277
278 public void setJiraServerId(String jiraServerId) {
279 this.jiraServerId = jiraServerId;
280 }
281
282
283
284
285
286
287 public void setFilter(String thisFilter) {
288 this.filter = thisFilter;
289 }
290
291
292
293
294
295
296 public void setComponent(String theseComponents) {
297 this.component = theseComponents;
298 }
299
300
301
302
303
304
305 public void setFixVersionIds(String theseFixVersionIds) {
306 this.fixVersionIds = theseFixVersionIds;
307 }
308
309
310
311
312
313
314 public void setTypeIds(String theseTypeIds) {
315 typeIds = theseTypeIds;
316 }
317
318 public void setConnectionTimeout(int connectionTimeout) {
319 this.connectionTimeout = connectionTimeout;
320 }
321
322 public void setReceiveTimout(int receiveTimout) {
323 this.receiveTimout = receiveTimout;
324 }
325
326 public static class NoRest extends Exception {
327 private static final long serialVersionUID = 6970088805270319624L;
328
329 public NoRest() {
330
331 }
332
333 public NoRest(String message) {
334 super(message);
335 }
336 }
337
338 public RestJiraDownloader() {
339 jsonFactory = new MappingJsonFactory();
340
341 dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
342 resolvedFixVersionIds = new ArrayList<>();
343 resolvedStatusIds = new ArrayList<>();
344 resolvedComponentIds = new ArrayList<>();
345 resolvedTypeIds = new ArrayList<>();
346 resolvedResolutionIds = new ArrayList<>();
347 resolvedPriorityIds = new ArrayList<>();
348 }
349
350 public void doExecute() throws Exception {
351
352 Map<String, String> urlMap =
353 JiraHelper.getJiraUrlAndProjectName(project.getIssueManagement().getUrl());
354 String jiraUrl = urlMap.get("url");
355 String jiraProject = urlMap.get("project");
356
357 try (CloseableHttpClient client = setupHttpClient(jiraUrl)) {
358 checkRestApi(client, jiraUrl);
359 doSessionAuth(client, jiraUrl);
360 resolveIds(client, jiraUrl, jiraProject);
361 search(client, jiraProject, jiraUrl);
362 }
363 }
364
365 private void search(CloseableHttpClient client, String jiraProject, String jiraUrl)
366 throws IOException, MojoExecutionException {
367 String jqlQuery = new JqlQueryBuilder(log)
368 .urlEncode(false)
369 .project(jiraProject)
370 .fixVersion(getFixFor())
371 .fixVersionIds(resolvedFixVersionIds)
372 .statusIds(resolvedStatusIds)
373 .priorityIds(resolvedPriorityIds)
374 .resolutionIds(resolvedResolutionIds)
375 .components(resolvedComponentIds)
376 .typeIds(resolvedTypeIds)
377 .sortColumnNames(sortColumnNames)
378 .filter(filter)
379 .build();
380
381 log.debug("JIRA jql=" + jqlQuery);
382
383 StringWriter searchParamStringWriter = new StringWriter();
384 try (JsonGenerator gen = jsonFactory.createGenerator(searchParamStringWriter)) {
385 gen.writeStartObject();
386 gen.writeStringField("jql", jqlQuery);
387 gen.writeNumberField("maxResults", nbEntriesMax);
388 gen.writeArrayFieldStart("fields");
389
390 gen.writeString("*all");
391 gen.writeEndArray();
392 gen.writeEndObject();
393 }
394
395 HttpPost httpPost = new HttpPost(jiraUrl + "/rest/api/2/search");
396 httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
397 httpPost.setEntity(new StringEntity(searchParamStringWriter.toString()));
398
399 try (CloseableHttpResponse response = client.execute(httpPost)) {
400
401 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
402 reportErrors(response);
403 }
404
405 JsonNode issueTree = getResponseTree(response);
406
407 assertIsObject(issueTree);
408 JsonNode issuesNode = issueTree.get("issues");
409 assertIsArray(issuesNode);
410 buildIssues(issuesNode, jiraUrl);
411 }
412 }
413
414 private void checkRestApi(CloseableHttpClient client, String jiraUrl) throws IOException, NoRest {
415
416
417
418
419
420 HttpGet httpGet = new HttpGet(jiraUrl + "/rest/api/2/serverInfo");
421 try (CloseableHttpResponse response = client.execute(httpGet)) {
422 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
423 throw new NoRest("This JIRA server does not support version 2 of the REST API, "
424 + "which maven-changes-plugin requires.");
425 }
426 }
427 }
428
429 private JsonNode getResponseTree(HttpResponse response) throws IOException {
430 try (InputStream inputStream = response.getEntity().getContent();
431 JsonParser jsonParser = jsonFactory.createParser(inputStream)) {
432 return jsonParser.readValueAsTree();
433 }
434 }
435
436 private void reportErrors(HttpResponse resp) throws IOException, MojoExecutionException {
437 ContentType contentType = ContentType.get(resp.getEntity());
438 if (contentType != null && contentType.getMimeType().equals(ContentType.APPLICATION_JSON.getMimeType())) {
439 JsonNode errorTree = getResponseTree(resp);
440 assertIsObject(errorTree);
441 JsonNode messages = errorTree.get("errorMessages");
442 if (messages != null) {
443 for (int mx = 0; mx < messages.size(); mx++) {
444 getLog().error(messages.get(mx).asText());
445 }
446 } else {
447 JsonNode message = errorTree.get("message");
448 if (message != null) {
449 getLog().error(message.asText());
450 }
451 }
452 }
453 throw new MojoExecutionException(String.format(
454 "Failed to query issues; response %d", resp.getStatusLine().getStatusCode()));
455 }
456
457 private void resolveIds(CloseableHttpClient client, String jiraUrl, String jiraProject)
458 throws IOException, MojoExecutionException, MojoFailureException {
459 resolveList(
460 resolvedComponentIds,
461 client,
462 "components",
463 component,
464 jiraUrl + "/rest/api/2/project/" + jiraProject + "/components");
465 resolveList(
466 resolvedFixVersionIds,
467 client,
468 "fixVersions",
469 fixVersionIds,
470 jiraUrl + "/rest/api/2/project/" + jiraProject + "/versions");
471 resolveList(resolvedStatusIds, client, "status", statusIds, jiraUrl + "/rest/api/2/status");
472 resolveList(resolvedResolutionIds, client, "resolution", resolutionIds, jiraUrl + "/rest/api/2/resolution");
473 resolveList(resolvedTypeIds, client, "type", typeIds, jiraUrl + "/rest/api/2/issuetype");
474 resolveList(resolvedPriorityIds, client, "priority", priorityIds, jiraUrl + "/rest/api/2/priority");
475 }
476
477 private void resolveList(
478 List<String> targetList, CloseableHttpClient client, String what, String input, String listRestUrlPattern)
479 throws IOException, MojoExecutionException, MojoFailureException {
480 if (input == null || input.isEmpty()) {
481 return;
482 }
483
484 HttpGet httpGet = new HttpGet(listRestUrlPattern);
485
486 try (CloseableHttpResponse response = client.execute(httpGet)) {
487 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
488 getLog().error(String.format("Could not get %s list from %s", what, listRestUrlPattern));
489 reportErrors(response);
490 }
491
492 JsonNode items = getResponseTree(response);
493 String[] pieces = input.split(",");
494 for (String item : pieces) {
495 targetList.add(resolveOneItem(items, what, item.trim()));
496 }
497 }
498 }
499
500 private String resolveOneItem(JsonNode items, String what, String nameOrId) throws MojoFailureException {
501 for (int cx = 0; cx < items.size(); cx++) {
502 JsonNode item = items.get(cx);
503 if (nameOrId.equals(item.get("id").asText())) {
504 return nameOrId;
505 } else if (nameOrId.equals(item.get("name").asText())) {
506 return item.get("id").asText();
507 }
508 }
509 throw new MojoFailureException(String.format("Could not find %s %s.", what, nameOrId));
510 }
511
512 private void buildIssues(JsonNode issuesNode, String jiraUrl) {
513 issueList = new ArrayList<>();
514 for (int ix = 0; ix < issuesNode.size(); ix++) {
515 JsonNode issueNode = issuesNode.get(ix);
516 assertIsObject(issueNode);
517 Issue issue = new Issue();
518 JsonNode val;
519
520 val = issueNode.get("id");
521 if (isNotNullNode(val)) {
522 issue.setId(val.asText());
523 }
524
525 val = issueNode.get("key");
526 if (isNotNullNode(val)) {
527 issue.setKey(val.asText());
528 issue.setLink(String.format("%s/browse/%s", jiraUrl, val.asText()));
529 }
530
531
532 JsonNode fieldsNode = issueNode.get("fields");
533
534 val = fieldsNode.get("assignee");
535 processAssignee(issue, val);
536
537 val = fieldsNode.get("created");
538 processCreated(issue, val);
539
540 val = fieldsNode.get("components");
541 processComponents(issue, val);
542
543 val = fieldsNode.get("fixVersions");
544 processFixVersions(issue, val);
545
546 val = fieldsNode.get("issuetype");
547 processIssueType(issue, val);
548
549 val = fieldsNode.get("priority");
550 processPriority(issue, val);
551
552 val = fieldsNode.get("reporter");
553 processReporter(issue, val);
554
555 val = fieldsNode.get("resolution");
556 processResolution(issue, val);
557
558 val = fieldsNode.get("status");
559 processStatus(issue, val);
560
561 val = fieldsNode.get("summary");
562 if (isNotNullNode(val)) {
563 issue.setSummary(val.asText());
564 }
565
566 val = fieldsNode.get("updated");
567 processUpdated(issue, val);
568
569 val = fieldsNode.get("versions");
570 processVersions(issue, val);
571
572 issueList.add(issue);
573 }
574 }
575
576 private void processVersions(Issue issue, JsonNode val) {
577 StringBuilder sb = new StringBuilder();
578 if (isNotNullNode(val)) {
579 for (int vx = 0; vx < val.size(); vx++) {
580 if (isNotNullNode(val.get(vx)) && isNotNullNode(val.get(vx).get("name"))) {
581 sb.append(val.get(vx).get("name").asText());
582 sb.append(", ");
583 }
584 }
585 }
586 if (sb.length() > 0) {
587
588 issue.setVersion(sb.substring(0, sb.length() - 2));
589 }
590 }
591
592 private void processStatus(Issue issue, JsonNode val) {
593 if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
594 issue.setStatus(val.get("name").asText());
595 }
596 }
597
598 private void processPriority(Issue issue, JsonNode val) {
599 if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
600 issue.setPriority(val.get("name").asText());
601 }
602 }
603
604 private void processResolution(Issue issue, JsonNode val) {
605 if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
606 issue.setResolution(val.get("name").asText());
607 }
608 }
609
610 private String getPerson(JsonNode val) {
611 JsonNode nameNode = val.get("displayName");
612 if (!isNotNullNode(nameNode)) {
613 nameNode = val.get("name");
614 }
615 if (isNotNullNode(nameNode)) {
616 return nameNode.asText();
617 } else {
618 return null;
619 }
620 }
621
622 private void processAssignee(Issue issue, JsonNode val) {
623 if (isNotNullNode(val)) {
624 String text = getPerson(val);
625 if (text != null) {
626 issue.setAssignee(text);
627 }
628 }
629 }
630
631 private void processReporter(Issue issue, JsonNode val) {
632 if (isNotNullNode(val)) {
633 String text = getPerson(val);
634 if (text != null) {
635 issue.setReporter(text);
636 }
637 }
638 }
639
640 private void processCreated(Issue issue, JsonNode val) {
641 if (isNotNullNode(val)) {
642 try {
643 issue.setCreated(parseDate(val));
644 } catch (ParseException e) {
645 getLog().warn("Invalid created date " + val.asText());
646 }
647 }
648 }
649
650 private void processUpdated(Issue issue, JsonNode val) {
651 if (isNotNullNode(val)) {
652 try {
653 issue.setUpdated(parseDate(val));
654 } catch (ParseException e) {
655 getLog().warn("Invalid updated date " + val.asText());
656 }
657 }
658 }
659
660 private Date parseDate(JsonNode val) throws ParseException {
661 return dateFormat.parse(val.asText());
662 }
663
664 private void processFixVersions(Issue issue, JsonNode val) {
665 if (isNotNullNode(val)) {
666 assertIsArray(val);
667 for (int vx = 0; vx < val.size(); vx++) {
668 JsonNode fvNode = val.get(vx);
669 if (isNotNullNode(fvNode) && isNotNullNode(fvNode.get("name"))) {
670 issue.addFixVersion(fvNode.get("name").asText());
671 }
672 }
673 }
674 }
675
676 private void processComponents(Issue issue, JsonNode val) {
677 if (isNotNullNode(val)) {
678 assertIsArray(val);
679 for (int cx = 0; cx < val.size(); cx++) {
680 JsonNode cnode = val.get(cx);
681 if (isNotNullNode(cnode) && isNotNullNode(cnode.get("name"))) {
682 issue.addComponent(cnode.get("name").asText());
683 }
684 }
685 }
686 }
687
688 private void processIssueType(Issue issue, JsonNode val) {
689 if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
690 issue.setType(val.get("name").asText());
691 }
692 }
693
694 private void assertIsObject(JsonNode node) {
695 if (!node.isObject()) {
696 throw new IllegalArgumentException("json node: " + node + " is not an object");
697 }
698 }
699
700 private void assertIsArray(JsonNode node) {
701 if (!node.isArray()) {
702 throw new IllegalArgumentException("json node: " + node + " is not an array");
703 }
704 }
705
706 private boolean isNotNullNode(JsonNode node) {
707 return node != null && !node.isNull();
708 }
709
710 private void doSessionAuth(CloseableHttpClient client, String jiraUrl)
711 throws IOException, MojoExecutionException, NoRest {
712
713 Server server = settings.getServer(jiraServerId);
714 if (server != null) {
715 SettingsDecryptionResult result = settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server));
716 if (!result.getProblems().isEmpty()) {
717 for (SettingsProblem problem : result.getProblems()) {
718 log.error(problem.getMessage());
719 }
720 } else {
721 jiraUser = result.getServer().getUsername();
722 jiraPassword = result.getServer().getPassword();
723 }
724 }
725
726 if (jiraUser != null) {
727 StringWriter jsWriter = new StringWriter();
728 try (JsonGenerator gen = jsonFactory.createGenerator(jsWriter)) {
729 gen.writeStartObject();
730 gen.writeStringField("username", jiraUser);
731 gen.writeStringField("password", jiraPassword);
732 gen.writeEndObject();
733 }
734
735 HttpPost post = new HttpPost(jiraUrl + "/rest/auth/1/session");
736 post.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
737 post.setEntity(new StringEntity(jsWriter.toString()));
738
739 try (CloseableHttpResponse response = client.execute(post)) {
740 int statusCode = response.getStatusLine().getStatusCode();
741 if (statusCode != HttpStatus.SC_OK) {
742 if (statusCode != HttpStatus.SC_UNAUTHORIZED && statusCode != HttpStatus.SC_FORBIDDEN) {
743
744
745 throw new NoRest();
746 }
747 throw new MojoExecutionException(String.format("Authentication failure status %d.", statusCode));
748 }
749 }
750 }
751 }
752
753 private CloseableHttpClient setupHttpClient(String jiraUrl) {
754
755 HttpClientBuilder httpClientBuilder = HttpClients.custom()
756 .setDefaultCookieStore(new BasicCookieStore())
757 .setDefaultRequestConfig(RequestConfig.custom()
758 .setConnectionRequestTimeout(receiveTimout)
759 .setConnectTimeout(connectionTimeout)
760 .build())
761 .setDefaultHeaders(Collections.singletonList(new BasicHeader("Accept", "application/json")));
762
763 Proxy proxy = getProxy(jiraUrl);
764 if (proxy != null) {
765 if (proxy.getUsername() != null && proxy.getPassword() != null) {
766 CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
767 credentialsProvider.setCredentials(
768 new AuthScope(proxy.getHost(), proxy.getPort()),
769 new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()));
770 httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
771 }
772 httpClientBuilder.setProxy(new HttpHost(proxy.getHost(), proxy.getPort()));
773 }
774 return httpClientBuilder.build();
775 }
776
777 private Proxy getProxy(String jiraUrl) {
778 Proxy proxy = settings.getActiveProxy();
779 if (proxy != null) {
780 SettingsDecryptionResult result = settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(proxy));
781 if (!result.getProblems().isEmpty()) {
782 for (SettingsProblem problem : result.getProblems()) {
783 log.error(problem.getMessage());
784 }
785 } else {
786 proxy = result.getProxy();
787 }
788 }
789
790 if (proxy != null && proxy.getNonProxyHosts() != null) {
791 URI jiraUri = URI.create(jiraUrl);
792 boolean nonProxy = Arrays.stream(proxy.getNonProxyHosts().split("\\|"))
793 .anyMatch(host -> !host.equalsIgnoreCase(jiraUri.getHost()));
794 if (nonProxy) {
795 return null;
796 }
797 }
798
799 return proxy;
800 }
801
802 public List<Issue> getIssueList() {
803 return issueList;
804 }
805 }