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