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 org.apache.commons.httpclient.Credentials;
23  import org.apache.commons.httpclient.Header;
24  import org.apache.commons.httpclient.HostConfiguration;
25  import org.apache.commons.httpclient.HttpClient;
26  import org.apache.commons.httpclient.HttpException;
27  import org.apache.commons.httpclient.HttpState;
28  import org.apache.commons.httpclient.HttpStatus;
29  import org.apache.commons.httpclient.StatusLine;
30  import org.apache.commons.httpclient.UsernamePasswordCredentials;
31  import org.apache.commons.httpclient.auth.AuthScope;
32  import org.apache.commons.httpclient.cookie.CookiePolicy;
33  import org.apache.commons.httpclient.methods.GetMethod;
34  import org.apache.commons.httpclient.params.HttpClientParams;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.issues.Issue;
37  import org.apache.maven.settings.Proxy;
38  import org.codehaus.plexus.util.IOUtil;
39  import org.codehaus.plexus.util.StringUtils;
40  
41  import java.io.FileOutputStream;
42  import java.io.IOException;
43  import java.io.InputStream;
44  import java.io.OutputStream;
45  import java.net.URLEncoder;
46  import java.util.Collections;
47  import java.util.List;
48  import java.util.Map;
49  
50  /**
51   * Gets relevant issues for a JIRA report via HTTP/RSS.
52   *
53   * @author mfranken@xebia.com
54   * @author jruiz@exist.com
55   * @version $Id: ClassicJiraDownloader.java 1428311 2013-01-03 12:16:29Z dennisl $
56   */
57  public final class ClassicJiraDownloader
58      extends AbstractJiraDownloader
59  {
60      public ClassicJiraDownloader()
61      {
62      }
63  
64      /**
65       * Execute the query on the JIRA server.
66       *
67       * @throws Exception on error
68       */
69      public void doExecute()
70          throws Exception
71      {
72          try
73          {
74              HttpClient client = new HttpClient();
75  
76              // MCHANGES-89 Allow circular redirects
77              HttpClientParams clientParams = client.getParams();
78              clientParams.setBooleanParameter( HttpClientParams.ALLOW_CIRCULAR_REDIRECTS, true );
79              clientParams.setCookiePolicy( CookiePolicy.BROWSER_COMPATIBILITY ); //MCHANGES-237
80  
81              HttpState state = new HttpState();
82  
83              HostConfiguration hc = new HostConfiguration();
84  
85              client.setHostConfiguration( hc );
86  
87              client.setState( state );
88  
89              String baseUrl = JiraHelper.getBaseUrl( project.getIssueManagement().getUrl() );
90  
91              getLog().debug( "JIRA lives at: " + baseUrl );
92              // Here we only need the host part of the URL
93              determineProxy( baseUrl, client );
94  
95              prepareBasicAuthentication( client );
96  
97              boolean jiraAuthenticationSuccessful = false;
98              if ( isJiraAuthenticationConfigured() )
99              {
100                 // Here we only need the parts up to and including the host part of the URL
101                 jiraAuthenticationSuccessful = doJiraAuthentication( client, baseUrl );
102             }
103 
104             if ( ( isJiraAuthenticationConfigured() && jiraAuthenticationSuccessful )
105                 || !isJiraAuthenticationConfigured() )
106             {
107                 String fullUrl = null;
108 
109                 if ( useJql )
110                 {
111                     fullUrl = getJqlQueryURL();
112                 }
113                 else
114                 {
115                     fullUrl = getParameterBasedQueryURL( client );
116                 }
117                 if ( log.isDebugEnabled() )
118                 {
119                     log.debug( "download jira issues from url " + fullUrl );
120                 }
121 
122                 // execute the GET
123                 download( client, fullUrl );
124             }
125         }
126         catch ( Exception e )
127         {
128             if ( project.getIssueManagement() != null )
129             {
130                 getLog().error( "Error accessing " + project.getIssueManagement().getUrl(), e );
131             }
132             else
133             {
134                 getLog().error( "Error accessing mock project issues", e );
135             }
136         }
137     }
138 
139     private String getJqlQueryURL()
140     {
141         // JQL is based on project names instead of project ID's
142         Map<String, String> urlMap = JiraHelper.getJiraUrlAndProjectName( project.getIssueManagement().getUrl() );
143         String jiraUrl = urlMap.get( "url" );
144         String jiraProject = urlMap.get( "project" );
145 
146         if ( jiraProject == null )
147         {
148             throw new RuntimeException( "The issue management URL in the POM does not include a JIRA project name" );
149         }
150         else
151         {
152             // create the URL for getting the proper issues from JIRA
153             String jqlQuery = new JqlQueryBuilder( log )
154                 .project( jiraProject )
155                 .fixVersion( getFixFor() )
156                 .fixVersionIds( fixVersionIds )
157                 .statusIds( statusIds )
158                 .priorityIds( priorityIds )
159                 .resolutionIds( resolutionIds )
160                 .components( component )
161                 .typeIds( typeIds )
162                 .sortColumnNames( sortColumnNames )
163                 .build();
164 
165             String url = new UrlBuilder( jiraUrl, "sr/jira.issueviews:searchrequest-xml/temp/SearchRequest.xml" )
166                 .addParameter( "tempMax", nbEntriesMax )
167                 .addParameter( "reset", "true" )
168                 .addParameter( "jqlQuery", jqlQuery )
169                 .build();
170 
171             return url;
172         }
173     }
174 
175     private String getParameterBasedQueryURL( HttpClient client )
176     {
177         Map<String, String> urlMap = JiraHelper.getJiraUrlAndProjectId( project.getIssueManagement().getUrl() );
178         String jiraUrl = urlMap.get( "url" );
179         String jiraId = urlMap.get( "id" );
180 
181         if ( jiraId == null || jiraId.length() == 0 )
182         {
183             log.debug( "The JIRA URL " + project.getIssueManagement().getUrl()
184                            + " doesn't include a pid, trying to extract it from JIRA." );
185             jiraId = JiraHelper.getPidFromJira( log, project.getIssueManagement().getUrl(), client );
186         }
187 
188         if ( jiraId == null )
189         {
190             throw new RuntimeException( "The issue management URL in the POM does not include a pid,"
191                                             + " and it was not possible to extract it from the page at that URL." );
192         }
193         else
194         {
195             // create the URL for getting the proper issues from JIRA
196             String fullURL = jiraUrl + "/secure/IssueNavigator.jspa?view=rss&pid=" + jiraId;
197 
198             if ( getFixFor() != null )
199             {
200                 fullURL += "&fixfor=" + getFixFor();
201             }
202 
203             String createdFilter = new ParameterQueryBuilder( log )
204                 .fixVersionIds( fixVersionIds )
205                 .statusIds( statusIds )
206                 .priorityIds( priorityIds )
207                 .resolutionIds( resolutionIds )
208                 .components( component )
209                 .typeIds( typeIds )
210                 .sortColumnNames( sortColumnNames )
211                 .filter( filter )
212                 .build();
213 
214             if ( createdFilter.charAt( 0 ) != '&' )
215             {
216                 fullURL += "&";
217             }
218             fullURL += createdFilter;
219 
220             fullURL += ( "&tempMax=" + nbEntriesMax + "&reset=true&decorator=none" );
221 
222             return fullURL;
223         }
224     }
225 
226     /**
227      * Check and prepare for basic authentication.
228      *
229      * @param client The client to prepare
230      */
231     private void prepareBasicAuthentication( HttpClient client )
232     {
233         if ( ( webUser != null ) && ( webUser.length() > 0 ) )
234         {
235             client.getParams().setAuthenticationPreemptive( true );
236 
237             Credentials defaultcreds = new UsernamePasswordCredentials( webUser, webPassword );
238 
239             getLog().debug( "Using username: " + webUser + " for Basic Authentication." );
240 
241             client.getState().setCredentials( new AuthScope( null, AuthScope.ANY_PORT, null, AuthScope.ANY_SCHEME ),
242                                               defaultcreds );
243         }
244     }
245 
246     /**
247      * Authenticate against JIRA. This method relies on jiraUser and
248      * jiraPassword being set. You can check this by calling
249      * isJiraAuthenticationConfigured().
250      *
251      * @param client    the HttpClient
252      * @param jiraUrl   the JIRA installation
253      * @return <code>true</code> if the authentication was successful, otherwise <code>false</code>
254      */
255     private boolean doJiraAuthentication( HttpClient client, final String jiraUrl )
256     {
257         // log into JIRA if we have to
258         String loginUrl = null;
259 
260         StringBuilder loginLink = new StringBuilder( jiraUrl );
261 
262         loginLink.append( "/login.jsp?os_destination=/secure/" );
263 
264         try
265         {
266             loginLink.append( "&os_username=" ).append( URLEncoder.encode( jiraUser, UTF_8 ) );
267 
268             String password = null;
269             if ( jiraPassword != null )
270             {
271                 password = StringUtils.repeat( "*", jiraPassword.length() );
272             }
273             getLog().debug( "Login URL: " + loginLink + "&os_password=" + password );
274 
275             loginLink.append( "&os_password=" ).append( URLEncoder.encode( jiraPassword, UTF_8 ) );
276 
277             loginUrl = loginLink.toString();
278 
279             // execute the login
280             GetMethod loginGet = new GetMethod( loginUrl );
281 
282             client.executeMethod( loginGet );
283 
284             if ( loginSucceeded( loginGet ) )
285             {
286                 getLog().debug( "Successfully logged in into JIRA." );
287                 return true;
288             }
289             else
290             {
291                 getLog().warn( "Was unable to login into JIRA: wrong username and/or password." );
292             }
293         }
294         catch ( Exception e )
295         {
296             if ( getLog().isDebugEnabled() )
297             {
298                 getLog().error( "Error trying to login into JIRA.", e );
299             }
300             else
301             {
302                 getLog().error( "Error trying to login into JIRA. Cause is: " + e.getLocalizedMessage() );
303             }
304         }
305         return false;
306     }
307 
308     /**
309      * Evaluate if the login attempt to JIRA was successful or not. We can't
310      * use the status code because JIRA returns 200 even if the login fails.
311      *
312      * @param loginGet The method that was executed
313      * @return <code>false</code> if we find an error message in the response body, otherwise <code>true</code>
314      * @todo There must be a nicer way to know whether we were able to login or not
315      */
316     private boolean loginSucceeded( GetMethod loginGet )
317         throws IOException
318     {
319         final String loginFailureResponse = "your username and password are incorrect";
320 
321         return loginGet.getResponseBodyAsString().indexOf( loginFailureResponse ) == -1;
322     }
323 
324     /**
325      * Setup proxy access if we have to.
326      *
327      * @param client  the HttpClient
328      */
329     private void determineProxy( String jiraUrl, HttpClient client )
330     {
331         // see whether there is any proxy defined in maven
332         Proxy proxy = null;
333 
334         getProxyInfo( jiraUrl );
335 
336         if ( proxyHost != null )
337         {
338             client.getHostConfiguration().setProxy( proxyHost, proxyPort );
339 
340             getLog().debug( "Using proxy: " + proxyHost + " at port " + proxyPort );
341 
342             if ( proxyUser != null )
343             {
344                 getLog().debug( "Using proxy user: " + proxyUser );
345 
346                 client.getState().setProxyCredentials(
347                     new AuthScope( null, AuthScope.ANY_PORT, null,
348                                    AuthScope.ANY_SCHEME ),
349                     new UsernamePasswordCredentials( proxyUser, proxyPass ) );
350             }
351         }
352     }
353 
354     /**
355      * Downloads the given link using the configured HttpClient, possibly following redirects.
356      *
357      * @param cl     the HttpClient
358      * @param link   the URL to JIRA
359      */
360     private void download( final HttpClient cl, final String link )
361     {
362         try
363         {
364             GetMethod gm = new GetMethod( link );
365 
366             getLog().info( "Downloading from JIRA at: " + link );
367 
368             gm.setFollowRedirects( true );
369 
370             cl.executeMethod( gm );
371 
372             StatusLine sl = gm.getStatusLine();
373 
374             if ( sl == null )
375             {
376                 getLog().error( "Unknown error validating link: " + link );
377 
378                 return;
379             }
380 
381             // if we get a redirect, do so
382             if ( gm.getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY )
383             {
384                 Header locationHeader = gm.getResponseHeader( "Location" );
385 
386                 if ( locationHeader == null )
387                 {
388                     getLog().warn( "Site sent redirect, but did not set Location header" );
389                 }
390                 else
391                 {
392                     String newLink = locationHeader.getValue();
393 
394                     getLog().debug( "Following redirect to " + newLink );
395 
396                     download( cl, newLink );
397                 }
398             }
399 
400             if ( gm.getStatusCode() == HttpStatus.SC_OK )
401             {
402                 final InputStream responseBodyStream = gm.getResponseBodyAsStream();
403 
404                 if ( !output.getParentFile().exists() )
405                 {
406                     output.getParentFile().mkdirs();
407                 }
408 
409                 // write the response to file
410                 OutputStream out = null;
411                 try
412                 {
413                     out = new FileOutputStream( output );
414                     IOUtil.copy( responseBodyStream, out );
415                 }
416                 finally
417                 {
418                     IOUtil.close( out );
419                     IOUtil.close( responseBodyStream );
420                 }
421 
422                 getLog().debug( "Downloading from JIRA was successful" );
423             }
424             else
425             {
426                 getLog().warn( "Downloading from JIRA failed. Received: [" + gm.getStatusCode() + "]" );
427             }
428         }
429         catch ( HttpException e )
430         {
431             if ( getLog().isDebugEnabled() )
432             {
433                 getLog().error( "Error downloading issues from JIRA:", e );
434             }
435             else
436             {
437                 getLog().error( "Error downloading issues from JIRA url: " + e.getLocalizedMessage() );
438 
439             }
440         }
441         catch ( IOException e )
442         {
443             if ( getLog().isDebugEnabled() )
444             {
445                 getLog().error( "Error downloading issues from JIRA:", e );
446             }
447             else
448             {
449                 getLog().error( "Error downloading issues from JIRA. Cause is " + e.getLocalizedMessage() );
450             }
451         }
452     }
453 
454     public List<Issue> getIssueList()
455         throws MojoExecutionException
456     {
457         if ( output.isFile() )
458         {
459             JiraXML jira = new JiraXML( log, jiraDatePattern );
460             jira.parseXML( output );
461             getLog().info( "The JIRA version is '" + jira.getJiraVersion() + "'" );
462             return jira.getIssueList();
463         }
464         else
465         {
466             getLog().warn( "JIRA file " + output.getPath() + " doesn't exist." );
467             return Collections.emptyList();
468         }
469     }
470 
471 }