blob: 69fa03b6181139ba310049119ede3e1632b215de [file] [log] [blame]
package org.apache.maven.plugins.jira;
/*
* 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.
*/
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;
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;
/**
* 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();
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();
gen.close();
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();
JsonGenerator gen = jsonFactory.createGenerator( jsWriter );
gen.writeStartObject();
gen.writeStringField( "username", jiraUser );
gen.writeStringField( "password", jiraPassword );
gen.writeEndObject();
gen.close();
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;
}
}