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 1586409 2014-04-10 18:48:13Z dennisl $
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             // create the URL for getting the proper issues from JIRA
152             String jqlQuery = new JqlQueryBuilder( log )
153                 .project( jiraProject )
154                 .fixVersion( getFixFor() )
155                 .fixVersionIds( fixVersionIds )
156                 .statusIds( statusIds )
157                 .priorityIds( priorityIds )
158                 .resolutionIds( resolutionIds )
159                 .components( component )
160                 .typeIds( typeIds )
161                 .sortColumnNames( sortColumnNames )
162                 .build();
163 
164             String url = new UrlBuilder( jiraUrl, "sr/jira.issueviews:searchrequest-xml/temp/SearchRequest.xml" )
165                 .addParameter( "tempMax", nbEntriesMax )
166                 .addParameter( "reset", "true" )
167                 .addParameter( "jqlQuery", jqlQuery )
168                 .build();
169 
170             return url;
171         }
172     }
173 
174     private String getParameterBasedQueryURL( HttpClient client )
175     {
176         Map<String, String> urlMap = JiraHelper.getJiraUrlAndProjectId( project.getIssueManagement().getUrl() );
177         String jiraUrl = urlMap.get( "url" );
178         String jiraId = urlMap.get( "id" );
179 
180         if ( jiraId == null || jiraId.length() == 0 )
181         {
182             log.debug( "The JIRA URL " + project.getIssueManagement().getUrl()
183                            + " doesn't include a pid, trying to extract it from JIRA." );
184             jiraId = JiraHelper.getPidFromJira( log, project.getIssueManagement().getUrl(), client );
185         }
186 
187         if ( jiraId == null )
188         {
189             throw new RuntimeException( "The issue management URL in the POM does not include a pid,"
190                                             + " and it was not possible to extract it from the page at that URL." );
191         }
192         else
193         {
194             // create the URL for getting the proper issues from JIRA
195             String fullURL = jiraUrl + "/secure/IssueNavigator.jspa?view=rss&pid=" + jiraId;
196 
197             if ( getFixFor() != null )
198             {
199                 fullURL += "&fixfor=" + getFixFor();
200             }
201 
202             String createdFilter = new ParameterQueryBuilder( log )
203                 .fixVersionIds( fixVersionIds )
204                 .statusIds( statusIds )
205                 .priorityIds( priorityIds )
206                 .resolutionIds( resolutionIds )
207                 .components( component )
208                 .typeIds( typeIds )
209                 .sortColumnNames( sortColumnNames )
210                 .filter( filter )
211                 .build();
212 
213             if ( createdFilter.charAt( 0 ) != '&' )
214             {
215                 fullURL += "&";
216             }
217             fullURL += createdFilter;
218 
219             fullURL += ( "&tempMax=" + nbEntriesMax + "&reset=true&decorator=none" );
220 
221             return fullURL;
222         }
223     }
224 
225     /**
226      * Check and prepare for basic authentication.
227      *
228      * @param client The client to prepare
229      */
230     private void prepareBasicAuthentication( HttpClient client )
231     {
232         if ( ( webUser != null ) && ( webUser.length() > 0 ) )
233         {
234             client.getParams().setAuthenticationPreemptive( true );
235 
236             Credentials defaultcreds = new UsernamePasswordCredentials( webUser, webPassword );
237 
238             getLog().debug( "Using username: " + webUser + " for Basic Authentication." );
239 
240             client.getState().setCredentials( new AuthScope( null, AuthScope.ANY_PORT, null, AuthScope.ANY_SCHEME ),
241                                               defaultcreds );
242         }
243     }
244 
245     /**
246      * Authenticate against JIRA. This method relies on jiraUser and
247      * jiraPassword being set. You can check this by calling
248      * isJiraAuthenticationConfigured().
249      *
250      * @param client    the HttpClient
251      * @param jiraUrl   the JIRA installation
252      * @return <code>true</code> if the authentication was successful, otherwise <code>false</code>
253      */
254     private boolean doJiraAuthentication( HttpClient client, final String jiraUrl )
255     {
256         // log into JIRA if we have to
257         String loginUrl;
258 
259         StringBuilder loginLink = new StringBuilder( jiraUrl );
260 
261         loginLink.append( "/login.jsp?os_destination=/secure/" );
262 
263         try
264         {
265             loginLink.append( "&os_username=" ).append( URLEncoder.encode( jiraUser, UTF_8 ) );
266 
267             String password = null;
268             if ( jiraPassword != null )
269             {
270                 password = StringUtils.repeat( "*", jiraPassword.length() );
271             }
272             getLog().debug( "Login URL: " + loginLink + "&os_password=" + password );
273 
274             loginLink.append( "&os_password=" ).append( URLEncoder.encode( jiraPassword, UTF_8 ) );
275 
276             loginUrl = loginLink.toString();
277 
278             // execute the login
279             GetMethod loginGet = new GetMethod( loginUrl );
280 
281             client.executeMethod( loginGet );
282 
283             if ( loginSucceeded( loginGet ) )
284             {
285                 getLog().debug( "Successfully logged in into JIRA." );
286                 return true;
287             }
288             else
289             {
290                 getLog().warn( "Was unable to login into JIRA: wrong username and/or password." );
291             }
292         }
293         catch ( Exception e )
294         {
295             if ( getLog().isDebugEnabled() )
296             {
297                 getLog().error( "Error trying to login into JIRA.", e );
298             }
299             else
300             {
301                 getLog().error( "Error trying to login into JIRA. Cause is: " + e.getLocalizedMessage() );
302             }
303         }
304         return false;
305     }
306 
307     /**
308      * Evaluate if the login attempt to JIRA was successful or not. We can't
309      * use the status code because JIRA returns 200 even if the login fails.
310      *
311      * @param loginGet The method that was executed
312      * @return <code>false</code> if we find an error message in the response body, otherwise <code>true</code>
313      * @todo There must be a nicer way to know whether we were able to login or not
314      */
315     private boolean loginSucceeded( GetMethod loginGet )
316         throws IOException
317     {
318         final String loginFailureResponse = "your username and password are incorrect";
319 
320         return !loginGet.getResponseBodyAsString().contains( loginFailureResponse );
321     }
322 
323     /**
324      * Setup proxy access if we have to.
325      *
326      * @param client  the HttpClient
327      */
328     private void determineProxy( String jiraUrl, HttpClient client )
329     {
330         // see whether there is any proxy defined in maven
331 
332         getProxyInfo( jiraUrl );
333 
334         if ( proxyHost != null )
335         {
336             client.getHostConfiguration().setProxy( proxyHost, proxyPort );
337 
338             getLog().debug( "Using proxy: " + proxyHost + " at port " + proxyPort );
339 
340             if ( proxyUser != null )
341             {
342                 getLog().debug( "Using proxy user: " + proxyUser );
343 
344                 client.getState().setProxyCredentials(
345                     new AuthScope( null, AuthScope.ANY_PORT, null,
346                                    AuthScope.ANY_SCHEME ),
347                     new UsernamePasswordCredentials( proxyUser, proxyPass ) );
348             }
349         }
350     }
351 
352     /**
353      * Downloads the given link using the configured HttpClient, possibly following redirects.
354      *
355      * @param cl     the HttpClient
356      * @param link   the URL to JIRA
357      */
358     private void download( final HttpClient cl, final String link )
359     {
360         try
361         {
362             GetMethod gm = new GetMethod( link );
363 
364             getLog().info( "Downloading from JIRA at: " + link );
365 
366             gm.setFollowRedirects( true );
367 
368             cl.executeMethod( gm );
369 
370             StatusLine sl = gm.getStatusLine();
371 
372             if ( sl == null )
373             {
374                 getLog().error( "Unknown error validating link: " + link );
375 
376                 return;
377             }
378 
379             // if we get a redirect, do so
380             if ( gm.getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY )
381             {
382                 Header locationHeader = gm.getResponseHeader( "Location" );
383 
384                 if ( locationHeader == null )
385                 {
386                     getLog().warn( "Site sent redirect, but did not set Location header" );
387                 }
388                 else
389                 {
390                     String newLink = locationHeader.getValue();
391 
392                     getLog().debug( "Following redirect to " + newLink );
393 
394                     download( cl, newLink );
395                 }
396             }
397 
398             if ( gm.getStatusCode() == HttpStatus.SC_OK )
399             {
400                 final InputStream responseBodyStream = gm.getResponseBodyAsStream();
401 
402                 if ( !output.getParentFile().exists() )
403                 {
404                     output.getParentFile().mkdirs();
405                 }
406 
407                 // write the response to file
408                 OutputStream out = null;
409                 try
410                 {
411                     out = new FileOutputStream( output );
412                     IOUtil.copy( responseBodyStream, out );
413                 }
414                 finally
415                 {
416                     IOUtil.close( out );
417                     IOUtil.close( responseBodyStream );
418                 }
419 
420                 getLog().debug( "Downloading from JIRA was successful" );
421             }
422             else
423             {
424                 getLog().warn( "Downloading from JIRA failed. Received: [" + gm.getStatusCode() + "]" );
425             }
426         }
427         catch ( HttpException e )
428         {
429             if ( getLog().isDebugEnabled() )
430             {
431                 getLog().error( "Error downloading issues from JIRA:", e );
432             }
433             else
434             {
435                 getLog().error( "Error downloading issues from JIRA url: " + e.getLocalizedMessage() );
436 
437             }
438         }
439         catch ( IOException e )
440         {
441             if ( getLog().isDebugEnabled() )
442             {
443                 getLog().error( "Error downloading issues from JIRA:", e );
444             }
445             else
446             {
447                 getLog().error( "Error downloading issues from JIRA. Cause is " + e.getLocalizedMessage() );
448             }
449         }
450     }
451 
452     public List<Issue> getIssueList()
453         throws MojoExecutionException
454     {
455         if ( output.isFile() )
456         {
457             JiraXML jira = new JiraXML( log, jiraDatePattern );
458             jira.parseXML( output );
459             getLog().info( "The JIRA version is '" + jira.getJiraVersion() + "'" );
460             return jira.getIssueList();
461         }
462         else
463         {
464             getLog().warn( "JIRA file " + output.getPath() + " doesn't exist." );
465             return Collections.emptyList();
466         }
467     }
468 
469 }