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