blob: 8fbc3557171bbbc783e2533b107d52c892ca3134 [file] [log] [blame]
/*
* 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.jira;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
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.cxf.configuration.security.AuthorizationPolicy;
import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxrs.client.ClientConfiguration;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.transports.http.configuration.ProxyServerType;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.issues.Issue;
/**
* Use the JIRA REST API to implement the download. This class assumes that the URL points to a copy of JIRA that
* implements the REST API. A static function may be forthcoming in here to probe and see if a given URL supports it.
*/
public class RestJiraDownloader extends AbstractJiraDownloader {
private List<Issue> issueList;
private JsonFactory jsonFactory;
private SimpleDateFormat dateFormat;
private List<String> resolvedFixVersionIds;
private List<String> resolvedStatusIds;
private List<String> resolvedComponentIds;
private List<String> resolvedTypeIds;
private List<String> resolvedResolutionIds;
private List<String> resolvedPriorityIds;
/**
*
*/
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<String, String> urlMap =
JiraHelper.getJiraUrlAndProjectName(project.getIssueManagement().getUrl());
String jiraUrl = urlMap.get("url");
String jiraProject = urlMap.get("project");
WebClient client = setupWebClient(jiraUrl);
// 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
client.replacePath("/rest/api/2/serverInfo");
client.accept(MediaType.APPLICATION_JSON);
Response siResponse = client.get();
if (siResponse.getStatus() != Response.Status.OK.getStatusCode()) {
throw new NoRest("This JIRA server does not support version 2 of the REST API, "
+ "which maven-changes-plugin requires.");
}
doSessionAuth(client);
resolveIds(client, jiraProject);
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();
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();
}
client.replacePath("/rest/api/2/search");
client.type(MediaType.APPLICATION_JSON_TYPE);
client.accept(MediaType.APPLICATION_JSON_TYPE);
Response searchResponse = client.post(searchParamStringWriter.toString());
if (searchResponse.getStatus() != Response.Status.OK.getStatusCode()) {
reportErrors(searchResponse);
}
JsonNode issueTree = getResponseTree(searchResponse);
assert issueTree.isObject();
JsonNode issuesNode = issueTree.get("issues");
assert issuesNode.isArray();
buildIssues(issuesNode, jiraUrl, jiraProject);
}
private JsonNode getResponseTree(Response response) throws IOException {
JsonParser jsonParser = jsonFactory.createParser((InputStream) response.getEntity());
return (JsonNode) jsonParser.readValueAsTree();
}
private void reportErrors(Response resp) throws IOException, MojoExecutionException {
if (MediaType.APPLICATION_JSON_TYPE
.getType()
.equals(getResponseMediaType(resp).getType())) {
JsonNode errorTree = getResponseTree(resp);
assert errorTree.isObject();
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.getStatus()));
}
private void resolveIds(WebClient client, String jiraProject)
throws IOException, MojoExecutionException, MojoFailureException {
resolveList(
resolvedComponentIds,
client,
"components",
component,
"/rest/api/2/project/{key}/components",
jiraProject);
resolveList(
resolvedFixVersionIds,
client,
"fixVersions",
fixVersionIds,
"/rest/api/2/project/{key}/versions",
jiraProject);
resolveList(resolvedStatusIds, client, "status", statusIds, "/rest/api/2/status");
resolveList(resolvedResolutionIds, client, "resolution", resolutionIds, "/rest/api/2/resolution");
resolveList(resolvedTypeIds, client, "type", typeIds, "/rest/api/2/issuetype");
resolveList(resolvedPriorityIds, client, "priority", priorityIds, "/rest/api/2/priority");
}
private void resolveList(
List<String> targetList,
WebClient client,
String what,
String input,
String listRestUrlPattern,
String... listUrlArgs)
throws IOException, MojoExecutionException, MojoFailureException {
if (input == null || input.length() == 0) {
return;
}
if (listUrlArgs != null && listUrlArgs.length != 0) {
client.replacePath("/");
client.path(listRestUrlPattern, listUrlArgs);
} else {
client.replacePath(listRestUrlPattern);
}
client.accept(MediaType.APPLICATION_JSON);
Response resp = client.get();
if (resp.getStatus() != Response.Status.OK.getStatusCode()) {
getLog().error(String.format("Could not get %s list from %s", what, listRestUrlPattern));
reportErrors(resp);
}
JsonNode items = getResponseTree(resp);
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 MediaType getResponseMediaType(Response response) {
String header = (String) response.getMetadata().getFirst(HttpHeaders.CONTENT_TYPE);
return header == null ? null : MediaType.valueOf(header);
}
private void buildIssues(JsonNode issuesNode, String jiraUrl, String jiraProject) {
issueList = new ArrayList<>();
for (int ix = 0; ix < issuesNode.size(); ix++) {
JsonNode issueNode = issuesNode.get(ix);
assert issueNode.isObject();
Issue issue = new Issue();
JsonNode val;
val = issueNode.get("id");
if (val != null) {
issue.setId(val.asText());
}
val = issueNode.get("key");
if (val != null) {
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("comment");
processComments(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 (val != null) {
issue.setSummary(val.asText());
}
val = fieldsNode.get("title");
if (val != null) {
issue.setTitle(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 (val != null) {
for (int vx = 0; vx < val.size(); vx++) {
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 (val != null) {
issue.setStatus(val.get("name").asText());
}
}
private void processPriority(Issue issue, JsonNode val) {
if (val != null) {
issue.setPriority(val.get("name").asText());
}
}
private void processResolution(Issue issue, JsonNode val) {
if (val != null) {
issue.setResolution(val.get("name").asText());
}
}
private String getPerson(JsonNode val) {
JsonNode nameNode = val.get("displayName");
if (nameNode == null) {
nameNode = val.get("name");
}
if (nameNode != null) {
return nameNode.asText();
} else {
return null;
}
}
private void processAssignee(Issue issue, JsonNode val) {
if (val != null) {
String text = getPerson(val);
if (text != null) {
issue.setAssignee(text);
}
}
}
private void processReporter(Issue issue, JsonNode val) {
if (val != null) {
String text = getPerson(val);
if (text != null) {
issue.setReporter(text);
}
}
}
private void processCreated(Issue issue, JsonNode val) {
if (val != null) {
try {
issue.setCreated(parseDate(val));
} catch (ParseException e) {
getLog().warn("Invalid created date " + val.asText());
}
}
}
private void processUpdated(Issue issue, JsonNode val) {
if (val != null) {
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 (val != null) {
assert val.isArray();
for (int vx = 0; vx < val.size(); vx++) {
JsonNode fvNode = val.get(vx);
issue.addFixVersion(fvNode.get("name").asText());
}
}
}
private void processComments(Issue issue, JsonNode val) {
if (val != null) {
JsonNode commentsArray = val.get("comments");
for (int cx = 0; cx < commentsArray.size(); cx++) {
JsonNode cnode = commentsArray.get(cx);
issue.addComment(cnode.get("body").asText());
}
}
}
private void processComponents(Issue issue, JsonNode val) {
if (val != null) {
assert val.isArray();
for (int cx = 0; cx < val.size(); cx++) {
JsonNode cnode = val.get(cx);
issue.addComponent(cnode.get("name").asText());
}
}
}
private void processIssueType(Issue issue, JsonNode val) {
if (val != null) {
issue.setType(val.get("name").asText());
}
}
private void doSessionAuth(WebClient client) throws IOException, MojoExecutionException, NoRest {
/* if JiraUser is specified instead of WebUser, we need to make a session. */
if (jiraUser != null) {
client.replacePath("/rest/auth/1/session");
client.type(MediaType.APPLICATION_JSON_TYPE);
StringWriter jsWriter = new StringWriter();
try (JsonGenerator gen = jsonFactory.createGenerator(jsWriter)) {
gen.writeStartObject();
gen.writeStringField("username", jiraUser);
gen.writeStringField("password", jiraPassword);
gen.writeEndObject();
}
Response authRes = client.post(jsWriter.toString());
if (authRes.getStatus() != Response.Status.OK.getStatusCode()) {
if (authRes.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode()
&& authRes.getStatus() != Response.Status.FORBIDDEN.getStatusCode()) {
// 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.", authRes.getStatus()));
}
}
}
private WebClient setupWebClient(String jiraUrl) {
WebClient client = WebClient.create(jiraUrl);
ClientConfiguration clientConfiguration = WebClient.getConfig(client);
HTTPConduit http = clientConfiguration.getHttpConduit();
// MCHANGES-324 - Maintain the client session
clientConfiguration.getRequestContext().put(Message.MAINTAIN_SESSION, Boolean.TRUE);
if (getLog().isDebugEnabled()) {
clientConfiguration.getInInterceptors().add(new LoggingInInterceptor());
clientConfiguration.getOutInterceptors().add(new LoggingOutInterceptor());
}
HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
// MCHANGES-341 Externalize JIRA server timeout values to the configuration section
getLog().debug("RestJiraDownloader: connectionTimeout: " + connectionTimeout);
httpClientPolicy.setConnectionTimeout(connectionTimeout);
httpClientPolicy.setAllowChunking(false);
getLog().debug("RestJiraDownloader: receiveTimout: " + receiveTimout);
httpClientPolicy.setReceiveTimeout(receiveTimout);
// MCHANGES-334 RestJiraDownloader doesn't honor proxy settings
getProxyInfo(jiraUrl);
if (proxyHost != null) {
getLog().debug("Using proxy: " + proxyHost + " at port " + proxyPort);
httpClientPolicy.setProxyServer(proxyHost);
httpClientPolicy.setProxyServerPort(proxyPort);
httpClientPolicy.setProxyServerType(ProxyServerType.HTTP);
if (proxyUser != null) {
ProxyAuthorizationPolicy proxyAuthorizationPolicy = new ProxyAuthorizationPolicy();
proxyAuthorizationPolicy.setAuthorizationType("Basic");
proxyAuthorizationPolicy.setUserName(proxyUser);
proxyAuthorizationPolicy.setPassword(proxyPass);
http.setProxyAuthorization(proxyAuthorizationPolicy);
}
}
if (webUser != null) {
AuthorizationPolicy authPolicy = new AuthorizationPolicy();
authPolicy.setAuthorizationType("Basic");
authPolicy.setUserName(webUser);
authPolicy.setPassword(webPassword);
http.setAuthorization(authPolicy);
}
http.setClient(httpClientPolicy);
return client;
}
public List<Issue> getIssueList() {
return issueList;
}
}