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 1742353 2016-05-05 03:22:53Z schulte $
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         InputStream in = null;
347         OutputStream out = null;
348         try
349         {
350             GetMethod gm = new GetMethod( link );
351 
352             getLog().info( "Downloading from JIRA at: " + link );
353 
354             gm.setFollowRedirects( true );
355 
356             cl.executeMethod( gm );
357 
358             StatusLine sl = gm.getStatusLine();
359 
360             if ( sl == null )
361             {
362                 getLog().error( "Unknown error validating link: " + link );
363 
364                 return;
365             }
366 
367             // if we get a redirect, do so
368             if ( gm.getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY )
369             {
370                 Header locationHeader = gm.getResponseHeader( "Location" );
371 
372                 if ( locationHeader == null )
373                 {
374                     getLog().warn( "Site sent redirect, but did not set Location header" );
375                 }
376                 else
377                 {
378                     String newLink = locationHeader.getValue();
379 
380                     getLog().debug( "Following redirect to " + newLink );
381 
382                     download( cl, newLink );
383                 }
384             }
385 
386             if ( gm.getStatusCode() == HttpStatus.SC_OK )
387             {
388                 in = gm.getResponseBodyAsStream();
389 
390                 if ( !output.getParentFile().exists() )
391                 {
392                     output.getParentFile().mkdirs();
393                 }
394 
395                 // write the response to file
396                 out = new FileOutputStream( output );
397                 IOUtil.copy( in, out );
398                 out.close();
399                 out = null;
400                 in.close();
401                 in = null;
402 
403                 getLog().debug( "Downloading from JIRA was successful" );
404             }
405             else
406             {
407                 getLog().warn( "Downloading from JIRA failed. Received: [" + gm.getStatusCode() + "]" );
408             }
409         }
410         catch ( HttpException e )
411         {
412             if ( getLog().isDebugEnabled() )
413             {
414                 getLog().error( "Error downloading issues from JIRA:", e );
415             }
416             else
417             {
418                 getLog().error( "Error downloading issues from JIRA url: " + e.getLocalizedMessage() );
419 
420             }
421         }
422         catch ( IOException e )
423         {
424             if ( getLog().isDebugEnabled() )
425             {
426                 getLog().error( "Error downloading issues from JIRA:", e );
427             }
428             else
429             {
430                 getLog().error( "Error downloading issues from JIRA. Cause is " + e.getLocalizedMessage() );
431             }
432         }
433         finally
434         {
435             IOUtil.close( out );
436             IOUtil.close( in );
437         }
438     }
439 
440     public List<Issue> getIssueList()
441         throws MojoExecutionException
442     {
443         if ( output.isFile() )
444         {
445             JiraXML jira = new JiraXML( log, jiraDatePattern );
446             jira.parseXML( output );
447             getLog().info( "The JIRA version is '" + jira.getJiraVersion() + "'" );
448             return jira.getIssueList();
449         }
450         else
451         {
452             getLog().warn( "JIRA file " + output.getPath() + " doesn't exist." );
453             return Collections.emptyList();
454         }
455     }
456 
457 }