
org.apache.maven.plugins.changes.jira.RestJiraDownloader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of maven-changes-plugin Show documentation
Show all versions of maven-changes-plugin Show documentation
Creates a release history for inclusion into the site and assists in generating an announcement mail.
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugins.changes.jira;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.changes.issues.Issue;
import org.apache.maven.plugins.changes.issues.IssueUtils;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.SettingsProblem;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
/**
* Use the JIRA REST API to download issues. This class assumes that the URL points to a copy of JIRA that
* implements the REST API.
*/
public class RestJiraDownloader {
/** Log for debug output. */
private Log log;
/** The maven project. */
private MavenProject project;
/** The maven settings. */
private Settings settings;
private SettingsDecrypter settingsDecrypter;
/** Filter the JIRA query based on the current version */
private boolean onlyCurrentVersion;
/** The versionPrefix to apply to the POM version */
protected String versionPrefix;
/** The maximum number of entries to show. */
protected int nbEntriesMax;
/** The filter to apply to query to JIRA. */
protected String filter;
/** Ids of fix versions to show, as comma separated string. */
protected String fixVersionIds;
/** Ids of status to show, as comma separated string. */
protected String statusIds;
/** Ids of resolution to show, as comma separated string. */
protected String resolutionIds;
/** Ids of priority to show, as comma separated string. */
protected String priorityIds;
/** The component to show. */
protected String component;
/** Ids of types to show, as comma separated string. */
protected String typeIds;
/** Column names to sort by, as comma separated string. */
protected String sortColumnNames;
/** The username to log into JIRA. */
protected String jiraUser;
/** The password to log into JIRA. */
protected String jiraPassword;
private String jiraServerId;
/** The pattern used to parse dates from the JIRA xml file. */
protected String jiraDatePattern;
protected int connectionTimeout;
protected int receiveTimout;
private List issueList;
private JsonFactory jsonFactory;
private SimpleDateFormat dateFormat;
private List resolvedFixVersionIds;
private List resolvedStatusIds;
private List resolvedComponentIds;
private List resolvedTypeIds;
private List resolvedResolutionIds;
private List resolvedPriorityIds;
/**
* Override this method if you need to get issues for a specific Fix For.
*
* @return a Fix For id or null
if you don't have that need
*/
protected String getFixFor() {
if (onlyCurrentVersion) {
// Let JIRA do the filtering of the current version instead of the JIRA mojo.
// This way JIRA returns less issues and we do not run into the "nbEntriesMax" limit that easily.
String version = (versionPrefix == null ? "" : versionPrefix) + project.getVersion();
// Remove "-SNAPSHOT" from the end of the version, if it's there
if (version.endsWith(IssueUtils.SNAPSHOT_SUFFIX)) {
return version.substring(0, version.length() - IssueUtils.SNAPSHOT_SUFFIX.length());
} else {
return version;
}
} else {
return null;
}
}
/**
* Sets the project.
*
* @param thisProject The project to set
*/
public void setMavenProject(MavenProject thisProject) {
this.project = thisProject;
}
public void setLog(Log log) {
this.log = log;
}
protected Log getLog() {
return log;
}
public void setSettings(Settings settings) {
this.settings = settings;
}
public void setSettingsDecrypter(SettingsDecrypter settingsDecrypter) {
this.settingsDecrypter = settingsDecrypter;
}
public void setOnlyCurrentVersion(boolean onlyCurrentVersion) {
this.onlyCurrentVersion = onlyCurrentVersion;
}
public void setVersionPrefix(String versionPrefix) {
this.versionPrefix = versionPrefix;
}
public void setJiraDatePattern(String jiraDatePattern) {
this.jiraDatePattern = jiraDatePattern;
}
/**
* Sets the maximum number of Issues to show.
*
* @param nbEntries The maximum number of Issues
*/
public void setNbEntries(final int nbEntries) {
nbEntriesMax = nbEntries;
}
/**
* Sets the statusIds.
*
* @param thisStatusIds The id(s) of the status to show, as comma separated string
*/
public void setStatusIds(String thisStatusIds) {
statusIds = thisStatusIds;
}
/**
* Sets the priorityIds.
*
* @param thisPriorityIds The id(s) of the priority to show, as comma separated string
*/
public void setPriorityIds(String thisPriorityIds) {
priorityIds = thisPriorityIds;
}
/**
* Sets the resolutionIds.
*
* @param thisResolutionIds The id(s) of the resolution to show, as comma separated string
*/
public void setResolutionIds(String thisResolutionIds) {
resolutionIds = thisResolutionIds;
}
/**
* Sets the sort column names.
*
* @param thisSortColumnNames The column names to sort by
*/
public void setSortColumnNames(String thisSortColumnNames) {
sortColumnNames = thisSortColumnNames;
}
/**
* Sets the password to log into a secured JIRA.
*
* @param thisJiraPassword The password for JIRA
*/
public void setJiraPassword(final String thisJiraPassword) {
this.jiraPassword = thisJiraPassword;
}
/**
* Sets the username to log into a secured JIRA.
*
* @param thisJiraUser The username for JIRA
*/
public void setJiraUser(String thisJiraUser) {
this.jiraUser = thisJiraUser;
}
public void setJiraServerId(String jiraServerId) {
this.jiraServerId = jiraServerId;
}
/**
* Sets the filter to apply to query to JIRA.
*
* @param thisFilter The filter to query JIRA
*/
public void setFilter(String thisFilter) {
this.filter = thisFilter;
}
/**
* Sets the component(s) to apply to query JIRA.
*
* @param theseComponents The id(s) of components to show, as comma separated string
*/
public void setComponent(String theseComponents) {
this.component = theseComponents;
}
/**
* Sets the fix version id(s) to apply to query JIRA.
*
* @param theseFixVersionIds The id(s) of fix versions to show, as comma separated string
*/
public void setFixVersionIds(String theseFixVersionIds) {
this.fixVersionIds = theseFixVersionIds;
}
/**
* Sets the typeIds.
*
* @param theseTypeIds The id(s) of the types to show, as comma separated string
*/
public void setTypeIds(String theseTypeIds) {
typeIds = theseTypeIds;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public void setReceiveTimout(int receiveTimout) {
this.receiveTimout = receiveTimout;
}
public static class NoRest extends Exception {
private static final long serialVersionUID = 6970088805270319624L;
public NoRest() {
// blank on purpose.
}
public NoRest(String message) {
super(message);
}
}
public RestJiraDownloader() {
jsonFactory = new MappingJsonFactory();
// 2012-07-17T06:26:47.723-0500
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
resolvedFixVersionIds = new ArrayList<>();
resolvedStatusIds = new ArrayList<>();
resolvedComponentIds = new ArrayList<>();
resolvedTypeIds = new ArrayList<>();
resolvedResolutionIds = new ArrayList<>();
resolvedPriorityIds = new ArrayList<>();
}
public void doExecute() throws Exception {
Map urlMap =
JiraHelper.getJiraUrlAndProjectName(project.getIssueManagement().getUrl());
String jiraUrl = urlMap.get("url");
String jiraProject = urlMap.get("project");
try (CloseableHttpClient client = setupHttpClient(jiraUrl)) {
checkRestApi(client, jiraUrl);
doSessionAuth(client, jiraUrl);
resolveIds(client, jiraUrl, jiraProject);
search(client, jiraProject, jiraUrl);
}
}
private void search(CloseableHttpClient client, String jiraProject, String jiraUrl)
throws IOException, MojoExecutionException {
String jqlQuery = new JqlQueryBuilder(log)
.urlEncode(false)
.project(jiraProject)
.fixVersion(getFixFor())
.fixVersionIds(resolvedFixVersionIds)
.statusIds(resolvedStatusIds)
.priorityIds(resolvedPriorityIds)
.resolutionIds(resolvedResolutionIds)
.components(resolvedComponentIds)
.typeIds(resolvedTypeIds)
.sortColumnNames(sortColumnNames)
.filter(filter)
.build();
log.debug("JIRA jql=" + jqlQuery);
StringWriter searchParamStringWriter = new StringWriter();
try (JsonGenerator gen = jsonFactory.createGenerator(searchParamStringWriter)) {
gen.writeStartObject();
gen.writeStringField("jql", jqlQuery);
gen.writeNumberField("maxResults", nbEntriesMax);
gen.writeArrayFieldStart("fields");
// Retrieve all fields. If that seems slow, we can reconsider.
gen.writeString("*all");
gen.writeEndArray();
gen.writeEndObject();
}
HttpPost httpPost = new HttpPost(jiraUrl + "/rest/api/2/search");
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
httpPost.setEntity(new StringEntity(searchParamStringWriter.toString()));
try (CloseableHttpResponse response = client.execute(httpPost)) {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
reportErrors(response);
}
JsonNode issueTree = getResponseTree(response);
assertIsObject(issueTree);
JsonNode issuesNode = issueTree.get("issues");
assertIsArray(issuesNode);
buildIssues(issuesNode, jiraUrl);
}
}
private void checkRestApi(CloseableHttpClient client, String jiraUrl) throws IOException, NoRest {
// We use version 2 of the REST API, that first appeared in JIRA 5
// Check if version 2 of the REST API is supported
// http://docs.atlassian.com/jira/REST/5.0/
// Note that serverInfo can always be accessed without authentication
HttpGet httpGet = new HttpGet(jiraUrl + "/rest/api/2/serverInfo");
try (CloseableHttpResponse response = client.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new NoRest("This JIRA server does not support version 2 of the REST API, "
+ "which maven-changes-plugin requires.");
}
}
}
private JsonNode getResponseTree(HttpResponse response) throws IOException {
try (InputStream inputStream = response.getEntity().getContent();
JsonParser jsonParser = jsonFactory.createParser(inputStream)) {
return jsonParser.readValueAsTree();
}
}
private void reportErrors(HttpResponse resp) throws IOException, MojoExecutionException {
ContentType contentType = ContentType.get(resp.getEntity());
if (contentType != null && contentType.getMimeType().equals(ContentType.APPLICATION_JSON.getMimeType())) {
JsonNode errorTree = getResponseTree(resp);
assertIsObject(errorTree);
JsonNode messages = errorTree.get("errorMessages");
if (messages != null) {
for (int mx = 0; mx < messages.size(); mx++) {
getLog().error(messages.get(mx).asText());
}
} else {
JsonNode message = errorTree.get("message");
if (message != null) {
getLog().error(message.asText());
}
}
}
throw new MojoExecutionException(String.format(
"Failed to query issues; response %d", resp.getStatusLine().getStatusCode()));
}
private void resolveIds(CloseableHttpClient client, String jiraUrl, String jiraProject)
throws IOException, MojoExecutionException, MojoFailureException {
resolveList(
resolvedComponentIds,
client,
"components",
component,
jiraUrl + "/rest/api/2/project/" + jiraProject + "/components");
resolveList(
resolvedFixVersionIds,
client,
"fixVersions",
fixVersionIds,
jiraUrl + "/rest/api/2/project/" + jiraProject + "/versions");
resolveList(resolvedStatusIds, client, "status", statusIds, jiraUrl + "/rest/api/2/status");
resolveList(resolvedResolutionIds, client, "resolution", resolutionIds, jiraUrl + "/rest/api/2/resolution");
resolveList(resolvedTypeIds, client, "type", typeIds, jiraUrl + "/rest/api/2/issuetype");
resolveList(resolvedPriorityIds, client, "priority", priorityIds, jiraUrl + "/rest/api/2/priority");
}
private void resolveList(
List targetList, CloseableHttpClient client, String what, String input, String listRestUrlPattern)
throws IOException, MojoExecutionException, MojoFailureException {
if (input == null || input.isEmpty()) {
return;
}
HttpGet httpGet = new HttpGet(listRestUrlPattern);
try (CloseableHttpResponse response = client.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
getLog().error(String.format("Could not get %s list from %s", what, listRestUrlPattern));
reportErrors(response);
}
JsonNode items = getResponseTree(response);
String[] pieces = input.split(",");
for (String item : pieces) {
targetList.add(resolveOneItem(items, what, item.trim()));
}
}
}
private String resolveOneItem(JsonNode items, String what, String nameOrId) throws MojoFailureException {
for (int cx = 0; cx < items.size(); cx++) {
JsonNode item = items.get(cx);
if (nameOrId.equals(item.get("id").asText())) {
return nameOrId;
} else if (nameOrId.equals(item.get("name").asText())) {
return item.get("id").asText();
}
}
throw new MojoFailureException(String.format("Could not find %s %s.", what, nameOrId));
}
private void buildIssues(JsonNode issuesNode, String jiraUrl) {
issueList = new ArrayList<>();
for (int ix = 0; ix < issuesNode.size(); ix++) {
JsonNode issueNode = issuesNode.get(ix);
assertIsObject(issueNode);
Issue issue = new Issue();
JsonNode val;
val = issueNode.get("id");
if (isNotNullNode(val)) {
issue.setId(val.asText());
}
val = issueNode.get("key");
if (isNotNullNode(val)) {
issue.setKey(val.asText());
issue.setLink(String.format("%s/browse/%s", jiraUrl, val.asText()));
}
// much of what we want is in here.
JsonNode fieldsNode = issueNode.get("fields");
val = fieldsNode.get("assignee");
processAssignee(issue, val);
val = fieldsNode.get("created");
processCreated(issue, val);
val = fieldsNode.get("components");
processComponents(issue, val);
val = fieldsNode.get("fixVersions");
processFixVersions(issue, val);
val = fieldsNode.get("issuetype");
processIssueType(issue, val);
val = fieldsNode.get("priority");
processPriority(issue, val);
val = fieldsNode.get("reporter");
processReporter(issue, val);
val = fieldsNode.get("resolution");
processResolution(issue, val);
val = fieldsNode.get("status");
processStatus(issue, val);
val = fieldsNode.get("summary");
if (isNotNullNode(val)) {
issue.setSummary(val.asText());
}
val = fieldsNode.get("updated");
processUpdated(issue, val);
val = fieldsNode.get("versions");
processVersions(issue, val);
issueList.add(issue);
}
}
private void processVersions(Issue issue, JsonNode val) {
StringBuilder sb = new StringBuilder();
if (isNotNullNode(val)) {
for (int vx = 0; vx < val.size(); vx++) {
if (isNotNullNode(val.get(vx)) && isNotNullNode(val.get(vx).get("name"))) {
sb.append(val.get(vx).get("name").asText());
sb.append(", ");
}
}
}
if (sb.length() > 0) {
// remove last ", "
issue.setVersion(sb.substring(0, sb.length() - 2));
}
}
private void processStatus(Issue issue, JsonNode val) {
if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
issue.setStatus(val.get("name").asText());
}
}
private void processPriority(Issue issue, JsonNode val) {
if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
issue.setPriority(val.get("name").asText());
}
}
private void processResolution(Issue issue, JsonNode val) {
if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
issue.setResolution(val.get("name").asText());
}
}
private String getPerson(JsonNode val) {
JsonNode nameNode = val.get("displayName");
if (!isNotNullNode(nameNode)) {
nameNode = val.get("name");
}
if (isNotNullNode(nameNode)) {
return nameNode.asText();
} else {
return null;
}
}
private void processAssignee(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
String text = getPerson(val);
if (text != null) {
issue.setAssignee(text);
}
}
}
private void processReporter(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
String text = getPerson(val);
if (text != null) {
issue.setReporter(text);
}
}
}
private void processCreated(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
try {
issue.setCreated(parseDate(val));
} catch (ParseException e) {
getLog().warn("Invalid created date " + val.asText());
}
}
}
private void processUpdated(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
try {
issue.setUpdated(parseDate(val));
} catch (ParseException e) {
getLog().warn("Invalid updated date " + val.asText());
}
}
}
private Date parseDate(JsonNode val) throws ParseException {
return dateFormat.parse(val.asText());
}
private void processFixVersions(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
assertIsArray(val);
for (int vx = 0; vx < val.size(); vx++) {
JsonNode fvNode = val.get(vx);
if (isNotNullNode(fvNode) && isNotNullNode(fvNode.get("name"))) {
issue.addFixVersion(fvNode.get("name").asText());
}
}
}
}
private void processComponents(Issue issue, JsonNode val) {
if (isNotNullNode(val)) {
assertIsArray(val);
for (int cx = 0; cx < val.size(); cx++) {
JsonNode cnode = val.get(cx);
if (isNotNullNode(cnode) && isNotNullNode(cnode.get("name"))) {
issue.addComponent(cnode.get("name").asText());
}
}
}
}
private void processIssueType(Issue issue, JsonNode val) {
if (isNotNullNode(val) && isNotNullNode(val.get("name"))) {
issue.setType(val.get("name").asText());
}
}
private void assertIsObject(JsonNode node) {
if (!node.isObject()) {
throw new IllegalArgumentException("json node: " + node + " is not an object");
}
}
private void assertIsArray(JsonNode node) {
if (!node.isArray()) {
throw new IllegalArgumentException("json node: " + node + " is not an array");
}
}
private boolean isNotNullNode(JsonNode node) {
return node != null && !node.isNull();
}
private void doSessionAuth(CloseableHttpClient client, String jiraUrl)
throws IOException, MojoExecutionException, NoRest {
Server server = settings.getServer(jiraServerId);
if (server != null) {
SettingsDecryptionResult result = settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server));
if (!result.getProblems().isEmpty()) {
for (SettingsProblem problem : result.getProblems()) {
log.error(problem.getMessage());
}
} else {
jiraUser = result.getServer().getUsername();
jiraPassword = result.getServer().getPassword();
}
}
if (jiraUser != null) {
StringWriter jsWriter = new StringWriter();
try (JsonGenerator gen = jsonFactory.createGenerator(jsWriter)) {
gen.writeStartObject();
gen.writeStringField("username", jiraUser);
gen.writeStringField("password", jiraPassword);
gen.writeEndObject();
}
HttpPost post = new HttpPost(jiraUrl + "/rest/auth/1/session");
post.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
post.setEntity(new StringEntity(jsWriter.toString()));
try (CloseableHttpResponse response = client.execute(post)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
if (statusCode != HttpStatus.SC_UNAUTHORIZED && statusCode != HttpStatus.SC_FORBIDDEN) {
// if not one of the documented failures, assume that there's no rest in there in the first
// place.
throw new NoRest();
}
throw new MojoExecutionException(String.format("Authentication failure status %d.", statusCode));
}
}
}
}
private CloseableHttpClient setupHttpClient(String jiraUrl) {
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setDefaultCookieStore(new BasicCookieStore())
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectionRequestTimeout(receiveTimout)
.setConnectTimeout(connectionTimeout)
.build())
.setDefaultHeaders(Collections.singletonList(new BasicHeader("Accept", "application/json")));
Proxy proxy = getProxy(jiraUrl);
if (proxy != null) {
if (proxy.getUsername() != null && proxy.getPassword() != null) {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
new AuthScope(proxy.getHost(), proxy.getPort()),
new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()));
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
httpClientBuilder.setProxy(new HttpHost(proxy.getHost(), proxy.getPort()));
}
return httpClientBuilder.build();
}
private Proxy getProxy(String jiraUrl) {
Proxy proxy = settings.getActiveProxy();
if (proxy != null) {
SettingsDecryptionResult result = settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(proxy));
if (!result.getProblems().isEmpty()) {
for (SettingsProblem problem : result.getProblems()) {
log.error(problem.getMessage());
}
} else {
proxy = result.getProxy();
}
}
if (proxy != null && proxy.getNonProxyHosts() != null) {
URI jiraUri = URI.create(jiraUrl);
boolean nonProxy = Arrays.stream(proxy.getNonProxyHosts().split("\\|"))
.anyMatch(host -> !host.equalsIgnoreCase(jiraUri.getHost()));
if (nonProxy) {
return null;
}
}
return proxy;
}
public List getIssueList() {
return issueList;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy