1 package org.apache.maven.jira;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import java.io.File;
21 import java.io.FileWriter;
22 import java.io.IOException;
23 import java.io.PrintWriter;
24
25 import java.text.NumberFormat;
26 import java.text.ParsePosition;
27
28 import java.util.HashMap;
29 import java.util.Map;
30
31 import org.apache.commons.httpclient.Credentials;
32 import org.apache.commons.httpclient.Header;
33 import org.apache.commons.httpclient.HostConfiguration;
34 import org.apache.commons.httpclient.HttpClient;
35 import org.apache.commons.httpclient.HttpException;
36 import org.apache.commons.httpclient.HttpState;
37 import org.apache.commons.httpclient.HttpStatus;
38 import org.apache.commons.httpclient.NTCredentials;
39 import org.apache.commons.httpclient.StatusLine;
40 import org.apache.commons.httpclient.UsernamePasswordCredentials;
41 import org.apache.commons.httpclient.methods.GetMethod;
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44 import org.apache.maven.jelly.MavenJellyContext;
45 import org.apache.maven.project.Project;
46
47 /**
48 * Gets relevant issues in RSS from a given JIRA installation.
49 *
50 * Based on version 1.1.2 and patch by Dr. Spock (MPJIRA-8)
51 *
52 * @author mfranken@xebia.com
53 */
54 public final class JiraDownloader
55 {
56 /**
57 * Log for debug output.
58 */
59 private static final Log LOG = LogFactory.getLog( JiraDownloader.class );
60
61 /** Mapping containing all JIRA status values. */
62 private static Map statusMap = new HashMap();
63
64 /** Mapping containing all JIRA resolution values. */
65 private static Map resolutionMap = new HashMap();
66
67 /** Mapping containing all JIRA priority values. */
68 private static Map priorityMap = new HashMap();
69
70 static
71 {
72 statusMap.put( "Open", "1" );
73 statusMap.put( "In Progress", "3" );
74 statusMap.put( "Reopened", "4" );
75 statusMap.put( "Resolved", "5" );
76 statusMap.put( "Closed", "6" );
77
78 resolutionMap.put( "Unresolved", "-1" );
79 resolutionMap.put( "Fixed", "1" );
80 resolutionMap.put( "Won't Fix", "2" );
81 resolutionMap.put( "Duplicate", "3" );
82 resolutionMap.put( "Incomplete", "4" );
83 resolutionMap.put( "Cannot Reproduce", "5" );
84
85 priorityMap.put( "Blocker", "1" );
86 priorityMap.put( "Critical", "2" );
87 priorityMap.put( "Major", "3" );
88 priorityMap.put( "Minor", "4" );
89 priorityMap.put( "Trivial", "5" );
90 }
91
92 /** Output file for xml document. */
93 private File output;
94
95 /** The maximum number of entries to show. */
96 private int nbEntriesMax;
97
98 /** The filter to apply to query to JIRA. */
99 private String filter;
100
101 /** Ids of status to show, as comma separated string. */
102 private String status;
103
104 /** Ids of resolution to show, as comma separated string. */
105 private String resolution;
106
107 /** Ids of priority to show, as comma separated string. */
108 private String priority;
109
110 /** The component to show. */
111 private String component;
112
113 /** The username to LOG into JIRA. */
114 private String jiraUser;
115
116 /** The password to LOG into JIRA. */
117 private String jiraPassword;
118
119 /** The username to LOG into webserver. */
120 private String webUser;
121
122 /** The password to LOG into webserver. */
123 private String webPassword;
124
125 /** The host to use if you are using NTLM authentication. */
126 private String proxyNtlmHost;
127
128 /** The NT domain to use if you are using NTLM authentication. */
129 private String proxyNtlmDomain;
130
131 /** The maven project. */
132 private Project project;
133
134 /** Include a Jira roadmap. */
135 private boolean roadmap;
136
137 /**
138 * Creates a filter given the maven.jira parameters and some defaults.
139 *
140 * @return request parameters to be added to URL used for downloading the JIRA issues
141 */
142 private String createFilter()
143 {
144 if ( ( this.filter != null ) && ( this.filter.length() > 0 ) )
145 {
146 if ( this.filter.charAt( 0 ) == '&' )
147 {
148 return this.filter.substring( 1 );
149 }
150
151 return this.filter;
152 }
153
154 StringBuffer localFilter = new StringBuffer();
155
156
157 if ( status != null )
158 {
159 String[] stats = status.split( "," );
160
161 for ( int i = 0; i < stats.length; i++ )
162 {
163 String statusParam = (String) statusMap.get( stats[i] );
164
165 if ( statusParam != null )
166 {
167 localFilter.append( "&status=" + statusParam );
168 }
169 }
170 }
171
172
173 if ( priority != null )
174 {
175 String[] prios = priority.split( "," );
176
177 for ( int i = 0; i < prios.length; i++ )
178 {
179 String priorityParam = (String) priorityMap.get( prios[i] );
180
181 if ( priorityParam != null )
182 {
183 localFilter.append( "&priority=" + priorityParam );
184 }
185 }
186 }
187
188 if ( resolution != null )
189 {
190
191 String[] resos = resolution.split( "," );
192
193 for ( int i = 0; i < resos.length; i++ )
194 {
195 String resoParam = (String) resolutionMap.get( resos[i] );
196
197 if ( resoParam != null )
198 {
199 localFilter.append( "&resolution=" + resoParam );
200 }
201 }
202 }
203
204
205 if ( component != null )
206 {
207 String[] components = component.split( "," );
208
209 for ( int i = 0; i < components.length; i++ )
210 {
211 if ( components[i].length() > 0 )
212 {
213 localFilter.append( "&component=" + components[i] );
214 }
215 }
216 }
217
218
219 String sort =
220 "&sorter/field=created&sorter/order=DESC"
221 + "&sorter/field=priority&sorter/order=DESC";
222
223 return localFilter + sort;
224 }
225
226 /**
227 * Execute the query on the JIRA server.
228 *
229 * @throws Exception
230 * on error
231 */
232 public void doExecute()
233 throws Exception
234 {
235 if ( project == null )
236 {
237 throw new Exception( "No project set." );
238 }
239
240 if ( project.getIssueTrackingUrl() == null )
241 {
242 throw new Exception( "No issue tracking url set." );
243 }
244
245 try
246 {
247 HttpClient cl = new HttpClient();
248 HttpState state = new HttpState();
249 HostConfiguration hc = new HostConfiguration();
250
251 cl.setHostConfiguration( hc );
252 cl.setState( state );
253
254 determineProxy( cl );
255
256
257 String url = project.getIssueTrackingUrl();
258
259
260 int pos = url.indexOf( "?" );
261
262
263 String id = null;
264
265 if ( pos >= 0 )
266 {
267
268 id = url.substring( url.lastIndexOf( "=" ) + 1 );
269 }
270
271 String jiraUrl = url.substring( 0, url.lastIndexOf( "/" ) );
272
273 if ( jiraUrl.endsWith( "secure" ) || jiraUrl.endsWith( "browse" ) )
274 {
275 jiraUrl = jiraUrl.substring( 0, jiraUrl.lastIndexOf( "/" ) );
276 }
277
278 LOG.info( "Jira lives at: " + jiraUrl );
279 doAuthentication( cl, jiraUrl );
280
281 String projectPage = "";
282
283 if ( ( id == null ) || roadmap )
284 {
285 GetMethod gm =
286 new GetMethod( url
287 + "?report=com.atlassian.jira.plugin.system.project:roadmap-panel" );
288
289 try
290 {
291 cl.executeMethod( gm );
292 LOG.info( "Succesfully reached JIRA." );
293 }
294 catch ( Exception e )
295 {
296 if ( LOG.isDebugEnabled() )
297 {
298 LOG.error( "Unable to reach JIRA project page:", e );
299 }
300 else
301 {
302 LOG.error(
303 "Unable to reach JIRA project page. Cause is: "
304 + e.getLocalizedMessage() );
305 }
306 }
307
308 projectPage = gm.getResponseBodyAsString();
309 }
310
311 if ( id == null )
312 {
313 LOG.info( "Jira URL " + url
314 + " doesn't include a pid, trying to get it" );
315
316 int pidIndex = projectPage.indexOf( "pid=" );
317
318 if ( pidIndex == -1 )
319 {
320
321 LOG.error( "Unable to get JIRA pid using url "
322 + project.getIssueTrackingUrl() );
323
324 return;
325 }
326
327 NumberFormat nf = NumberFormat.getInstance();
328 Number pidNumber =
329 nf.parse( projectPage, new ParsePosition( pidIndex + 4 ) );
330
331 id = Integer.toString( pidNumber.intValue() );
332 }
333
334
335 String fullURL =
336 jiraUrl + "/secure/IssueNavigator.jspa?view=rss&pid=" + id;
337
338 fullURL += createFilter();
339 fullURL += ( "&tempMax=" + nbEntriesMax
340 + "&reset=true&decorator=none" );
341
342
343 download( cl, fullURL, output );
344
345 if ( roadmap )
346 {
347 int fixforIndex = projectPage.indexOf( "fixfor=" );
348
349 if ( fixforIndex == -1 )
350 {
351
352 LOG.error( "Unable to get JIRA roadmap using url "
353 + project.getIssueTrackingUrl() );
354
355 return;
356 }
357
358 NumberFormat nf = NumberFormat.getInstance();
359 Number fixforNumber =
360 nf.parse( projectPage, new ParsePosition( fixforIndex + 7 ) );
361 String fixfor = Integer.toString( fixforNumber.intValue() );
362
363
364 if ( !fixfor.equals( "-1" ) )
365 {
366 setFilter( "&&fixfor=" + fixfor
367 + "&sorter/field=status&sorter/order=ASC" );
368 fullURL =
369 jiraUrl + "/secure/IssueNavigator.jspa?view=rss&pid=" + id;
370 fullURL += createFilter();
371 fullURL += ( "&tempMax=" + nbEntriesMax
372 + "&reset=true&decorator=none" );
373
374 String outFile = output.getAbsolutePath().replace('\\','/');
375 int endIndex = outFile.lastIndexOf( '/' );
376
377 outFile =
378 outFile.substring( 0, endIndex ) + "/jira-roadmap.xml";
379
380
381 download( cl, fullURL, new File( outFile ) );
382 }
383 }
384 }
385 catch ( Exception e )
386 {
387 LOG.error( "Error accessing " + project.getIssueTrackingUrl(), e );
388 }
389 }
390
391 /**
392 * Authenticate against webserver and into JIRA if we have to.
393 *
394 * @param client
395 * the HttpClient
396 * @param jiraUrl
397 * the JIRA installation
398 */
399 private void doAuthentication( HttpClient client, final String jiraUrl )
400 {
401
402 if ( ( webUser != null ) && ( webUser.length() > 0 ) )
403 {
404 client.getState().setAuthenticationPreemptive( true );
405
406 Credentials defaultcreds;
407 if ( this.proxyNtlmHost != null && this.proxyNtlmDomain != null)
408 {
409 defaultcreds =
410 new NTCredentials( webUser, webPassword,
411 proxyNtlmHost, proxyNtlmDomain );
412 }
413 else
414 {
415 defaultcreds =
416 new UsernamePasswordCredentials( webUser, webPassword );
417 }
418
419 LOG.info( "Using username: " + webUser
420 + " for Basic Authentication against the webserver at "
421 + jiraUrl );
422 client.getState().setCredentials( null, null, defaultcreds );
423 }
424
425
426 String loginUrl = null;
427
428 if ( ( jiraUser != null ) && ( jiraUser.length() > 0 )
429 && ( jiraPassword != null ) )
430 {
431 StringBuffer loginLink = new StringBuffer( jiraUrl );
432
433 loginLink.append( "/login.jsp?os_destination=/secure/" );
434 loginLink.append( "&os_username=" ).append( jiraUser );
435 LOG.info( "Login URL: " + loginLink + "&os_password=*******" );
436 loginLink.append( "&os_password=" ).append( jiraPassword );
437 loginUrl = loginLink.toString();
438 }
439
440
441 if ( loginUrl != null )
442 {
443 GetMethod loginGet = new GetMethod( loginUrl );
444
445 try
446 {
447 client.executeMethod( loginGet );
448 LOG.info( "Succesfully logged in into JIRA." );
449 }
450 catch ( Exception e )
451 {
452 if ( LOG.isDebugEnabled() )
453 {
454 LOG.error( "Error trying to login into JIRA:", e );
455 }
456 else
457 {
458 LOG.error( "Error trying to login into JIRA. Cause is: "
459 + e.getLocalizedMessage() );
460 }
461
462
463 }
464 }
465 }
466
467 /**
468 * Setup proxy access if we have to.
469 *
470 * @param client
471 * the HttpClient
472 */
473 private void determineProxy( HttpClient client )
474 {
475
476 if ( project == null )
477 {
478 LOG.error( "No project set. No proxy info available." );
479
480 return;
481 }
482
483 MavenJellyContext ctx = project.getContext();
484
485 if ( ctx == null )
486 {
487 LOG.error( "Maven project has no context. No proxy info available." );
488
489 return;
490 }
491
492 String proxyHost = ctx.getProxyHost();
493
494 if ( proxyHost != null )
495 {
496 String proxyPort = ctx.getProxyPort();
497
498 client.getHostConfiguration().setProxy( proxyHost,
499 Integer.parseInt( proxyPort ) );
500 LOG.info( "Using proxy: " + proxyHost + " at port " + proxyPort );
501
502 String proxyUser = ctx.getProxyUserName();
503
504 if ( proxyUser != null )
505 {
506 LOG.info( "Using proxy user: " + proxyUser );
507
508 String proxyPass = ctx.getProxyPassword();
509
510 client.getState().setProxyCredentials( null, null,
511 new UsernamePasswordCredentials( proxyUser, proxyPass ) );
512 }
513 }
514 }
515
516 /**
517 * Downloads the given link using the configured HttpClient, possibly following redirects.
518 *
519 * @param cl
520 * the HttpClient
521 * @param link
522 * the JiraUrl
523 * @param outFile
524 * the output file
525 * @return
526 */
527 private void download( final HttpClient cl, final String link,
528 final File outFile )
529 {
530 try
531 {
532 GetMethod gm = new GetMethod( link );
533
534 LOG.info( "Downloading " + link );
535 gm.setFollowRedirects( true );
536 cl.executeMethod( gm );
537
538 final String strGetResponseBody = gm.getResponseBodyAsString();
539
540
541 PrintWriter pw = new PrintWriter( new FileWriter( outFile ) );
542
543 pw.print( strGetResponseBody );
544 pw.close();
545
546 StatusLine sl = gm.getStatusLine();
547
548 if ( sl == null )
549 {
550 LOG.info( "Unknown error validating link : " + link );
551
552 return;
553 }
554
555
556 if ( gm.getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY )
557 {
558 Header locationHeader = gm.getResponseHeader( "Location" );
559
560 if ( locationHeader == null )
561 {
562 LOG.info( "Site sent redirect, but did not set Location header" );
563 }
564 else
565 {
566 String newLink = locationHeader.getValue();
567
568 LOG.debug( "Following redirect to " + newLink );
569 download( cl, newLink, outFile );
570 }
571 }
572
573 if ( gm.getStatusCode() != HttpStatus.SC_OK )
574 {
575 LOG.warn( "Received: [" + gm.getStatusCode() + "]" );
576 }
577 }
578 catch ( HttpException e )
579 {
580 if ( LOG.isDebugEnabled() )
581 {
582 LOG.error( "Error downloading issues from JIRA:", e );
583 }
584 else
585 {
586 LOG.error( "Error downloading issues from JIRA. Cause is: "
587 + e.getLocalizedMessage() );
588 }
589 }
590 catch ( IOException e )
591 {
592 if ( LOG.isDebugEnabled() )
593 {
594 LOG.error( "Error downloading issues from JIRA:", e );
595 }
596 else
597 {
598 LOG.error( "Error downloading issues from JIRA. Cause is: "
599 + e.getLocalizedMessage() );
600 }
601 }
602 }
603
604 /**
605 * Set the output file for the LOG.
606 *
607 * @param thisOutput
608 * the output file
609 */
610 public void setOutput( final File thisOutput )
611 {
612 this.output = thisOutput;
613 }
614
615 /**
616 * Sets the project.
617 *
618 * @param thisProject
619 * The project to set
620 */
621 public void setProject( final Object thisProject )
622 {
623 this.project = (Project) thisProject;
624 }
625
626 /**
627 * Sets the maximum number of Issues to show.
628 *
629 * @param nbEntries
630 * The maximum number of Issues
631 */
632 public void setNbEntries( final int nbEntries )
633 {
634 nbEntriesMax = nbEntries;
635 }
636
637 /**
638 * Sets the status.
639 *
640 * @param thisStatus
641 * The id(s) of the status to show, as comma separated string
642 */
643 public void setStatus( final String thisStatus )
644 {
645 status = thisStatus;
646 }
647
648 /**
649 * Sets the priority.
650 *
651 * @param thisPriority
652 * The id(s) of the priority to show, as comma separated string
653 */
654 public void setPriority( final String thisPriority )
655 {
656 priority = thisPriority;
657 }
658
659 /**
660 * Sets the resolution.
661 *
662 * @param thisResolution
663 * The id(s) of the resolution to show, as comma separated string
664 */
665 public void setResolution( final String thisResolution )
666 {
667 resolution = thisResolution;
668 }
669
670 /**
671 * Sets the password for authentication against the webserver.
672 *
673 * @param thisWebPassword
674 * The password of the webserver
675 */
676 public void setWebPassword( final String thisWebPassword )
677 {
678 this.webPassword = thisWebPassword;
679 }
680
681 /**
682 * Sets the username for authentication against the webserver.
683 *
684 * @param thisWebUser
685 * The username of the webserver
686 */
687 public void setWebUser( final String thisWebUser )
688 {
689 this.webUser = thisWebUser;
690 }
691
692 /**
693 * Sets the NTLM host.
694 *
695 * @param thisProxyNtlmHost The NTLM host to set.
696 */
697 public void setProxyNtlmHost( final String thisProxyNtlmHost )
698 {
699 this.proxyNtlmHost = thisProxyNtlmHost;
700 }
701
702 /**
703 * Sets the NTLM domain.
704 *
705 * @param thisProxyNtlmDomain The NTLM domain to set.
706 */
707 public void setProxyNtlmDomain( final String thisProxyNtlmDomain )
708 {
709 this.proxyNtlmDomain = thisProxyNtlmDomain;
710 }
711
712 /**
713 * Sets the password to LOG into a secured JIRA.
714 *
715 * @param thisJiraPassword
716 * The password for JIRA
717 */
718 public void setJiraPassword( final String thisJiraPassword )
719 {
720 this.jiraPassword = thisJiraPassword;
721 }
722
723 /**
724 * Sets the username to LOG into a secured JIRA.
725 *
726 * @param thisJiraUser
727 * The username for JIRA
728 */
729 public void setJiraUser( final String thisJiraUser )
730 {
731 this.jiraUser = thisJiraUser;
732 }
733
734 /**
735 * Sets the filter to apply to query to JIRA.
736 *
737 * @param thisFilter
738 * The filter to query JIRA
739 */
740 public void setFilter( final String thisFilter )
741 {
742 this.filter = thisFilter;
743 }
744
745 /**
746 * Sets the component(s) to apply to query JIRA.
747 *
748 * @param theseComponents
749 * The id(s) of components to show, as comma separated string
750 */
751 public void setComponent( final String theseComponents )
752 {
753 this.component = theseComponents;
754 }
755
756 /**
757 * Sets the roadmap property.
758 * @param thisRoadmap The roadmap.
759 */
760 public void setRoadmap( final boolean thisRoadmap )
761 {
762 this.roadmap = thisRoadmap;
763 }
764 }