| /* |
| * 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; |
| } |
| } |