View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.maven.plugin.jira;
21  
22  import com.fasterxml.jackson.core.JsonFactory;
23  import com.fasterxml.jackson.core.JsonGenerator;
24  import com.fasterxml.jackson.core.JsonParser;
25  import com.fasterxml.jackson.databind.JsonNode;
26  import com.fasterxml.jackson.databind.MappingJsonFactory;
27  import org.apache.cxf.configuration.security.AuthorizationPolicy;
28  import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
29  import org.apache.cxf.interceptor.LoggingInInterceptor;
30  import org.apache.cxf.interceptor.LoggingOutInterceptor;
31  import org.apache.cxf.jaxrs.client.ClientConfiguration;
32  import org.apache.cxf.jaxrs.client.WebClient;
33  import org.apache.cxf.transport.http.HTTPConduit;
34  import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
35  import org.apache.cxf.transports.http.configuration.ProxyServerType;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugin.MojoFailureException;
38  import org.apache.maven.plugin.issues.Issue;
39  
40  import javax.ws.rs.core.HttpHeaders;
41  import javax.ws.rs.core.MediaType;
42  import javax.ws.rs.core.Response;
43  import java.io.IOException;
44  import java.io.InputStream;
45  import java.io.StringWriter;
46  import java.text.ParseException;
47  import java.text.SimpleDateFormat;
48  import java.util.ArrayList;
49  import java.util.Date;
50  import java.util.List;
51  import java.util.Map;
52  
53  /**
54   * Use the JIRA REST API to implement the download.
55   *
56   * This class assumes that the URL points to a copy of JIRA that implements the REST API.
57   * A static function may be forthcoming in here to probe and see if a given URL supports it.
58   *
59   */
60  public class RestJiraDownloader extends AbstractJiraDownloader
61  {
62      private List<Issue> issueList;
63      private JsonFactory jsonFactory;
64      private SimpleDateFormat dateFormat;
65  
66      private List<String> resolvedFixVersionIds;
67      private List<String> resolvedStatusIds;
68      private List<String> resolvedComponentIds;
69      private List<String> resolvedTypeIds;
70      private List<String> resolvedResolutionIds;
71      private List<String> resolvedPriorityIds;
72  
73      private String jiraProject;
74  
75      public static class NoRest extends Exception {
76          public NoRest( )
77          {
78              // blank on purpose.
79          }
80          public NoRest( String message )
81          {
82              super( message );
83          }
84      }
85  
86      public RestJiraDownloader() {
87          jsonFactory = new MappingJsonFactory(  );
88          //2012-07-17T06:26:47.723-0500
89          dateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" );
90          resolvedFixVersionIds = new ArrayList<String>(  );
91          resolvedStatusIds = new ArrayList<String>(  );
92          resolvedComponentIds = new ArrayList<String>(  );
93          resolvedTypeIds = new ArrayList<String>(  );
94          resolvedResolutionIds = new ArrayList<String>(  );
95          resolvedPriorityIds = new ArrayList<String>(  );
96      }
97  
98      public void doExecute()
99          throws Exception
100     {
101 
102         Map<String, String> urlMap = JiraHelper.getJiraUrlAndProjectName( project.getIssueManagement().getUrl() );
103         String jiraUrl = urlMap.get( "url" );
104         jiraProject = urlMap.get( "project" );
105 
106         // This classloader juggling is a workaround for a classic Maven 2 class loader management bug.
107         ClassLoader ccl = Thread.currentThread().getContextClassLoader();
108         try
109         {
110             Thread.currentThread().setContextClassLoader( WebClient.class.getClassLoader( ) );
111             WebClient client = setupWebClient( jiraUrl );
112 
113             // We use version 2 of the REST API, that first appeared in JIRA 5
114             // Check if version 2 of the REST API is supported
115             // http://docs.atlassian.com/jira/REST/5.0/
116             // Note that serverInfo can always be accessed without authentication
117             client.replacePath( "/rest/api/2/serverInfo" );
118             client.accept( MediaType.APPLICATION_JSON );
119             Response siResponse = client.get();
120             if ( siResponse.getStatus() != Response.Status.OK.getStatusCode() )
121             {
122                 throw new NoRest(
123                     "This JIRA server does not support version 2 of the REST API, which maven-changes-plugin requires." );
124             }
125 
126             doSessionAuth( client );
127 
128             resolveIds( client, jiraProject );
129 
130             String jqlQuery = new JqlQueryBuilder( log ).urlEncode( false ).project( jiraProject ).fixVersion(
131                 getFixFor() ).fixVersionIds( resolvedFixVersionIds ).statusIds( resolvedStatusIds ).priorityIds(
132                 resolvedPriorityIds ).resolutionIds( resolvedResolutionIds ).components( resolvedComponentIds ).typeIds(
133                 resolvedTypeIds ).sortColumnNames( sortColumnNames ).build();
134 
135             StringWriter searchParamStringWriter = new StringWriter();
136             JsonGenerator gen = jsonFactory.createGenerator( searchParamStringWriter );
137             gen.writeStartObject();
138             gen.writeStringField( "jql", jqlQuery );
139             gen.writeNumberField( "maxResults", nbEntriesMax );
140             gen.writeArrayFieldStart( "fields" );
141             // Retrieve all fields. If that seems slow, we can reconsider.
142             gen.writeString( "*all" );
143             gen.writeEndArray();
144             gen.writeEndObject();
145             gen.close();
146             client.replacePath( "/rest/api/2/search" );
147             client.type( MediaType.APPLICATION_JSON_TYPE );
148             client.accept( MediaType.APPLICATION_JSON_TYPE );
149             Response searchResponse = client.post( searchParamStringWriter.toString() );
150             if ( searchResponse.getStatus() != Response.Status.OK.getStatusCode() )
151             {
152                 reportErrors( searchResponse );
153             }
154 
155             JsonNode issueTree = getResponseTree( searchResponse );
156             assert issueTree.isObject();
157             JsonNode issuesNode = issueTree.get( "issues" );
158             assert issuesNode.isArray();
159             buildIssues( issuesNode, jiraUrl, jiraProject );
160         }
161         finally
162         {
163             Thread.currentThread().setContextClassLoader( ccl );
164         }
165     }
166 
167     private JsonNode getResponseTree( Response response )
168         throws IOException
169     {
170         JsonParser jsonParser = jsonFactory.createJsonParser( (InputStream) response.getEntity() );
171         return (JsonNode) jsonParser.readValueAsTree();
172     }
173 
174     private void reportErrors( Response resp )
175         throws IOException, MojoExecutionException
176     {
177         if ( MediaType.APPLICATION_JSON_TYPE.getType().equals( getResponseMediaType( resp ).getType() ) )
178         {
179             JsonNode errorTree = getResponseTree( resp );
180             assert errorTree.isObject();
181             JsonNode messages = errorTree.get( "errorMessages" );
182             if ( messages != null )
183             {
184                 for ( int mx = 0; mx < messages.size(); mx ++ )
185                 {
186                     getLog().error( messages.get( mx ).asText() );
187                 }
188             }
189             else
190             {
191                 JsonNode message = errorTree.get( "message" );
192                 if ( message != null )
193                 {
194                     getLog().error( message.asText() );
195                 }
196             }
197         }
198         throw new MojoExecutionException( String.format( "Failed to query issues; response %d", resp.getStatus() ) );
199     }
200 
201     private void resolveIds( WebClient client, String jiraProject )
202         throws IOException, MojoExecutionException, MojoFailureException
203     {
204         resolveList( resolvedComponentIds, client, "components",  component, "/rest/api/2/project/{key}/components", jiraProject );
205         resolveList( resolvedFixVersionIds, client, "fixVersions", fixVersionIds, "/rest/api/2/project/{key}/versions", jiraProject );
206         resolveList( resolvedStatusIds, client, "status", statusIds, "/rest/api/2/status" );
207         resolveList( resolvedResolutionIds, client, "resolution", resolutionIds, "/rest/api/2/resolution" );
208         resolveList( resolvedTypeIds, client, "type", typeIds, "/rest/api/2/issuetype" );
209         resolveList( resolvedPriorityIds, client, "priority", priorityIds, "/rest/api/2/priority" );
210     }
211 
212     private void resolveList( List<String> targetList, WebClient client, String what, String input,
213                               String listRestUrlPattern, String... listUrlArgs )
214         throws IOException, MojoExecutionException, MojoFailureException
215     {
216         if ( input == null || input.length() == 0 )
217         {
218             return;
219         }
220         if ( listUrlArgs != null && listUrlArgs.length != 0)
221         {
222             client.replacePath( "/" );
223             client.path( listRestUrlPattern, listUrlArgs );
224         }
225         else
226         {
227             client.replacePath( listRestUrlPattern );
228         }
229         client.accept( MediaType.APPLICATION_JSON );
230         Response resp = client.get();
231         if ( resp.getStatus() != 200 )
232         {
233             getLog().error( String.format( "Could not get %s list from %s", what, listRestUrlPattern ) );
234             reportErrors( resp );
235         }
236 
237         JsonNode items = getResponseTree( resp );
238         String[] pieces = input.split( "," );
239         for (String item : pieces ) {
240             targetList.add( resolveOneItem( items, what, item ) );
241         }
242     }
243 
244     private String resolveOneItem( JsonNode items, String what, String nameOrId )
245         throws IOException, MojoExecutionException, MojoFailureException
246     {
247         for ( int cx = 0; cx < items.size(); cx ++ )
248         {
249             JsonNode item = items.get( cx );
250             if ( nameOrId.equals( item.get( "id" ).asText() ) )
251             {
252                 return nameOrId;
253             }
254             else if ( nameOrId.equals( item.get( "name" ).asText() ) )
255             {
256                 return item.get( "id" ).asText();
257             }
258         }
259         throw new MojoFailureException( String.format("Could not find %s %s.", what, nameOrId ) );
260     }
261 
262     private MediaType getResponseMediaType( Response response )
263     {
264         String header = (String) response.getMetadata().getFirst( HttpHeaders.CONTENT_TYPE ) ;
265         return header == null ? null : MediaType.valueOf( header );
266     }
267 
268     private void buildIssues( JsonNode issuesNode, String jiraUrl, String jiraProject )
269     {
270         issueList = new ArrayList<Issue>(  );
271         for ( int ix = 0; ix < issuesNode.size(); ix++ )
272         {
273             JsonNode issueNode = issuesNode.get( ix );
274             assert issueNode.isObject();
275             Issue issue = new Issue();
276             JsonNode val;
277 
278             val = issueNode.get( "id" );
279             if ( val != null )
280             {
281                 issue.setId( val.asText() );
282             }
283 
284             val = issueNode.get( "key" );
285             if ( val != null )
286             {
287                 issue.setKey( val.asText() );
288                 issue.setLink( String.format( "%s/browse/%s", jiraUrl, val.asText()) );
289             }
290 
291             // much of what we want is in here.
292             JsonNode fieldsNode = issueNode.get( "fields" );
293 
294             val = fieldsNode.get( "assignee" );
295             processAssignee( issue, val );
296 
297             val = fieldsNode.get( "created" );
298             processCreated( issue, val );
299 
300             val = fieldsNode.get( "comment" );
301             processComments( issue, val );
302 
303             val = fieldsNode.get( "fixVersions" );
304             processFixVersions( issue, val );
305 
306 
307             val = fieldsNode.get( "priority" );
308             processPriority( issue, val );
309 
310             val = fieldsNode.get( "reporter" );
311             processReporter( issue, val );
312 
313             val = fieldsNode.get( "resolution" );
314             processResolution( issue, val );
315 
316             val = fieldsNode.get( "status" );
317             processStatus( issue, val );
318 
319             val = fieldsNode.get( "summary" );
320             if ( val != null )
321             {
322                 issue.setSummary( val.asText() );
323             }
324 
325             val = issueNode.get( "title" );
326             if ( val != null )
327             {
328                 issue.setTitle( val.asText() );
329             }
330 
331             val = issueNode.get( "updated" );
332             processUpdated( issue, val );
333 
334             val = issueNode.get( "versions" );
335             processVersions( issue, val );
336 
337 
338             issueList.add( issue );
339         }
340     }
341 
342     private void processVersions( Issue issue, JsonNode val )
343     {
344         StringBuilder sb = new StringBuilder( );
345         if ( val != null )
346         {
347             for ( int vx = 0; vx < val.size(); vx ++ )
348             {
349                 sb.append( val.get( vx ).get( "name" ).asText() );
350                 sb.append( ", " );
351             }
352         }
353         if ( sb.length() > 0 )
354         {
355             // remove last ", "
356             issue.setVersion( sb.substring( 0, sb.length() - 2 ) );
357         }
358     }
359 
360     private void processStatus( Issue issue, JsonNode val )
361     {
362         if (val != null )
363         {
364             issue.setStatus( val.get( "name" ).asText() );
365         }
366     }
367 
368     private void processPriority( Issue issue, JsonNode val )
369     {
370         if (val != null )
371         {
372             issue.setPriority( val.get( "name" ).asText() );
373         }
374     }
375 
376     private void processResolution( Issue issue, JsonNode val )
377     {
378         if (val != null )
379         {
380             issue.setResolution( val.get( "name" ).asText() );
381         }
382     }
383 
384     private String getPerson( JsonNode val )
385     {
386         JsonNode nameNode = val.get( "displayName" );
387         if ( nameNode == null )
388         {
389             nameNode = val.get( "name" );
390         }
391         if ( nameNode != null )
392         {
393             return nameNode.asText();
394         }
395         else
396         {
397             return null;
398         }
399     }
400 
401     private void processAssignee( Issue issue, JsonNode val )
402     {
403         if ( val != null )
404         {
405             String text = getPerson( val );
406             if ( text != null )
407             {
408                 issue.setAssignee( text );
409             }
410         }
411     }
412 
413     private void processReporter( Issue issue, JsonNode val )
414     {
415         if ( val != null )
416         {
417             String text = getPerson( val );
418             if ( text != null )
419             {
420                 issue.setReporter( text );
421             }
422         }
423     }
424 
425     private void processCreated( Issue issue, JsonNode val )
426     {
427         if ( val != null )
428         {
429             try
430             {
431                 issue.setCreated( parseDate( val ) );
432             }
433             catch ( ParseException e )
434             {
435                 getLog().warn( "Invalid created date " + val.asText() );
436             }
437         }
438     }
439 
440     private void processUpdated( Issue issue, JsonNode val )
441     {
442         if ( val != null )
443         {
444             try
445             {
446                 issue.setUpdated( parseDate( val ) );
447             }
448             catch ( ParseException e )
449             {
450                 getLog().warn( "Invalid created date " + val.asText() );
451             }
452         }
453     }
454 
455     private Date parseDate( JsonNode val )
456         throws ParseException
457     {
458         return dateFormat.parse( val.asText() );
459     }
460 
461     private void processFixVersions( Issue issue, JsonNode val )
462     {
463         if (val != null)
464         {
465             assert val.isArray();
466             for ( int vx = 0; vx < val.size(); vx++ )
467             {
468                 JsonNode fvNode = val.get( vx );
469                 issue.addFixVersion( fvNode.get( "name" ).asText() );
470             }
471         }
472     }
473 
474     private void processComments( Issue issue, JsonNode val )
475     {
476         if ( val != null )
477         {
478             JsonNode commentsArray = val.get( "comments" );
479             for ( int cx = 0; cx < commentsArray.size(); cx++ )
480             {
481                 JsonNode cnode = commentsArray.get( cx );
482                 issue.addComment( cnode.get( "body" ).asText() );
483             }
484         }
485     }
486 
487     private void doSessionAuth( WebClient client )
488         throws IOException, MojoExecutionException, NoRest
489     {
490         /* if JiraUser is specified instead of WebUser, we need to make a session. */
491         if ( jiraUser != null )
492         {
493             client.replacePath( "/rest/auth/1/session" );
494             client.type( MediaType.APPLICATION_JSON_TYPE );
495             StringWriter jsWriter = new StringWriter( );
496             JsonGenerator gen = jsonFactory.createGenerator( jsWriter );
497             gen.writeStartObject();
498             gen.writeStringField( "username", jiraUser );
499             gen.writeStringField( "password", jiraPassword );
500             gen.writeEndObject();
501             gen.close();
502             Response authRes = client.post( jsWriter.toString() );
503             if ( authRes.getStatus() != Response.Status.OK.getStatusCode() )
504             {
505                 if ( authRes.getStatus() != 401 && authRes.getStatus() != 403 )
506                 {
507                     // if not one of the documented failures, assume that there's no rest in there in the first place.
508                     throw new NoRest();
509                 }
510                 throw new MojoExecutionException( String.format( "Authentication failure status %d.", authRes.getStatus() ) );
511             }
512         }
513     }
514 
515     private WebClient setupWebClient( String jiraUrl )
516     {
517         WebClient client = WebClient.create( jiraUrl );
518 
519         ClientConfiguration clientConfiguration = WebClient.getConfig( client );
520         HTTPConduit http = clientConfiguration.getHttpConduit();
521 
522         if ( getLog().isDebugEnabled() )
523         {
524             clientConfiguration.getInInterceptors().add( new LoggingInInterceptor(  ) );
525             clientConfiguration.getOutInterceptors().add( new LoggingOutInterceptor(  ) );
526         }
527 
528         HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
529 
530         httpClientPolicy.setConnectionTimeout(36000);
531         httpClientPolicy.setAllowChunking(false);
532         httpClientPolicy.setReceiveTimeout(32000);
533 
534         if ( proxyHost != null )
535         {
536             getLog().debug( "Using proxy: " + proxyHost + " at port " + proxyPort );
537             httpClientPolicy.setProxyServer( proxyHost );
538             httpClientPolicy.setProxyServerPort( proxyPort );
539             httpClientPolicy.setProxyServerType( ProxyServerType.HTTP );
540             if ( proxyUser != null )
541             {
542                 ProxyAuthorizationPolicy proxyAuthorizationPolicy = new ProxyAuthorizationPolicy();
543                 proxyAuthorizationPolicy.setAuthorizationType( "Basic" );
544                 proxyAuthorizationPolicy.setUserName( proxyUser );
545                 proxyAuthorizationPolicy.setPassword( proxyPass );
546                 http.setProxyAuthorization( proxyAuthorizationPolicy );
547             }
548         }
549 
550         if ( webUser != null )
551         {
552             AuthorizationPolicy authPolicy = new AuthorizationPolicy();
553             authPolicy.setAuthorizationType( "Basic" );
554             authPolicy.setUserName( webUser );
555             authPolicy.setPassword( webPassword );
556             http.setAuthorization( authPolicy );
557         }
558 
559         http.setClient(httpClientPolicy);
560         return client;
561     }
562 
563     public List<Issue> getIssueList() throws MojoExecutionException
564     {
565         return issueList;
566     }
567 }