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