View Javadoc

1   package org.apache.maven.wagon.shared.http;
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.HttpConnectionManager;
27  import org.apache.commons.httpclient.HttpException;
28  import org.apache.commons.httpclient.HttpMethod;
29  import org.apache.commons.httpclient.HttpStatus;
30  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
31  import org.apache.commons.httpclient.NTCredentials;
32  import org.apache.commons.httpclient.UsernamePasswordCredentials;
33  import org.apache.commons.httpclient.auth.AuthScope;
34  import org.apache.commons.httpclient.methods.GetMethod;
35  import org.apache.commons.httpclient.methods.HeadMethod;
36  import org.apache.commons.httpclient.methods.PutMethod;
37  import org.apache.commons.httpclient.methods.RequestEntity;
38  import org.apache.commons.httpclient.params.HttpMethodParams;
39  import org.apache.commons.httpclient.util.DateParseException;
40  import org.apache.commons.httpclient.util.DateUtil;
41  import org.apache.maven.wagon.InputData;
42  import org.apache.maven.wagon.OutputData;
43  import org.apache.maven.wagon.PathUtils;
44  import org.apache.maven.wagon.ResourceDoesNotExistException;
45  import org.apache.maven.wagon.StreamWagon;
46  import org.apache.maven.wagon.TransferFailedException;
47  import org.apache.maven.wagon.Wagon;
48  import org.apache.maven.wagon.authorization.AuthorizationException;
49  import org.apache.maven.wagon.events.TransferEvent;
50  import org.apache.maven.wagon.proxy.ProxyInfo;
51  import org.apache.maven.wagon.repository.Repository;
52  import org.apache.maven.wagon.resource.Resource;
53  import org.codehaus.plexus.util.IOUtil;
54  import org.codehaus.plexus.util.StringUtils;
55  
56  import java.io.File;
57  import java.io.FileInputStream;
58  import java.io.FileOutputStream;
59  import java.io.IOException;
60  import java.io.InputStream;
61  import java.io.OutputStream;
62  import java.net.URLEncoder;
63  import java.text.SimpleDateFormat;
64  import java.util.Date;
65  import java.util.Iterator;
66  import java.util.Locale;
67  import java.util.Properties;
68  import java.util.TimeZone;
69  import java.util.zip.GZIPInputStream;
70  
71  /**
72   * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
73   * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
74   */
75  public abstract class AbstractHttpClientWagon
76      extends StreamWagon
77  {
78      private final class RequestEntityImplementation
79          implements RequestEntity
80      {
81          private final Resource resource;
82  
83          private final Wagon wagon;
84  
85          private final File source;
86  
87          private RequestEntityImplementation( final InputStream stream, final Resource resource, final Wagon wagon, final File source )
88              throws TransferFailedException
89          {
90              if ( source != null )
91              {
92                  this.source = source;
93              }
94              else
95              {
96                  FileOutputStream fos = null;
97                  try
98                  {
99                      this.source = File.createTempFile( "http-wagon.", ".tmp" );
100                     this.source.deleteOnExit();
101                     
102                     fos = new FileOutputStream( this.source );
103                     IOUtil.copy( stream, fos );
104                 }
105                 catch ( IOException e )
106                 {
107                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
108                     throw new TransferFailedException( "Failed to buffer stream contents to temp file for upload.", e );
109                 }
110                 finally
111                 {
112                     IOUtil.close( fos );
113                 }
114             }
115             
116             this.resource = resource;
117             this.wagon = wagon;
118         }
119 
120         public long getContentLength()
121         {
122             return resource.getContentLength();
123         }
124 
125         public String getContentType()
126         {
127             return null;
128         }
129 
130         public boolean isRepeatable()
131         {
132             return true;
133         }
134 
135         public void writeRequest( OutputStream output )
136             throws IOException
137         {
138             byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
139             
140             TransferEvent transferEvent =
141                 new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
142             transferEvent.setTimestamp( System.currentTimeMillis() );
143             
144             FileInputStream fin = null;
145             try
146             {
147                 fin = new FileInputStream( source );
148                 int remaining = Integer.MAX_VALUE;
149                 while ( remaining > 0 )
150                 {
151                     int n = fin.read( buffer, 0, Math.min( buffer.length, remaining ) );
152                 
153                     if ( n == -1 )
154                     {
155                         break;
156                     }
157                 
158                     fireTransferProgress( transferEvent, buffer, n );
159                 
160                     output.write( buffer, 0, n );
161                 
162                     remaining -= n;
163                 }
164             }
165             finally
166             {
167                 IOUtil.close( fin );
168             }
169             
170             output.flush();
171         }
172     }
173 
174     protected static final int SC_NULL = -1;
175 
176     protected static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
177 
178     private HttpClient client;
179 
180     protected HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
181 
182     /**
183      * @deprecated Use httpConfiguration instead.
184      */
185     private Properties httpHeaders;
186     
187     /**
188      * @since 1.0-beta-6
189      */
190     private HttpConfiguration httpConfiguration;
191 
192     private HttpMethod getMethod;
193 
194     public void openConnectionInternal()
195     {
196         repository.setUrl( getURL( repository ) );
197         client = new HttpClient( connectionManager );
198         String username = null;
199         String password = null;
200 
201         if ( authenticationInfo != null )
202         {
203             username = authenticationInfo.getUserName();
204 
205             password = authenticationInfo.getPassword();
206         }
207 
208         String host = getRepository().getHost();
209 
210         if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
211         {
212             Credentials creds = new UsernamePasswordCredentials( username, password );
213 
214             int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
215             
216             AuthScope scope = new AuthScope( host, port );
217             client.getState().setCredentials( scope, creds );
218         }
219 
220         HostConfiguration hc = new HostConfiguration();
221 
222         ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
223         if ( proxyInfo != null )
224         {
225             String proxyUsername = proxyInfo.getUserName();
226             String proxyPassword = proxyInfo.getPassword();
227             String proxyHost = proxyInfo.getHost();
228             int proxyPort = proxyInfo.getPort();
229             String proxyNtlmHost = proxyInfo.getNtlmHost();
230             String proxyNtlmDomain = proxyInfo.getNtlmDomain();
231             if ( proxyHost != null )
232             {
233                 hc.setProxy( proxyHost, proxyPort );
234 
235                 if ( proxyUsername != null && proxyPassword != null )
236                 {
237                     Credentials creds;
238                     if ( proxyNtlmHost != null || proxyNtlmDomain != null )
239                     {
240                         creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
241                     }
242                     else
243                     {
244                         creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
245                     }
246 
247                     int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
248                     
249                     AuthScope scope = new AuthScope( proxyHost, port );
250                     client.getState().setProxyCredentials( scope, creds );
251                 }
252             }
253         }
254 
255         hc.setHost( host );
256 
257         //start a session with the webserver
258         client.setHostConfiguration( hc );
259     }
260 
261     public void closeConnection()
262     {
263         if ( connectionManager instanceof MultiThreadedHttpConnectionManager )
264         {
265             ( (MultiThreadedHttpConnectionManager) connectionManager ).shutdown();
266         }
267     }
268 
269     public void put( File source, String resourceName )
270         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
271     {
272         Resource resource = new Resource( resourceName );
273         
274         firePutInitiated( resource, source );
275         
276         resource.setContentLength( source.length() );
277         
278         resource.setLastModified( source.lastModified() );
279 
280         put( null, resource, source );
281     }
282     
283     public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
284         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
285     {
286         Resource resource = new Resource( destination );
287         
288         firePutInitiated( resource, null );
289         
290         resource.setContentLength( contentLength );
291         
292         resource.setLastModified( lastModified );
293         
294         put( stream, resource, null );
295     }
296 
297     private void put( final InputStream stream, Resource resource, File source )
298         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
299     {
300         String url = getRepository().getUrl();
301         String[] parts = StringUtils.split( resource.getName(), "/" );
302         for ( int i = 0; i < parts.length; i++ )
303         {
304             // TODO: Fix encoding...
305             // url += "/" + URLEncoder.encode( parts[i], System.getProperty("file.encoding") );
306             url += "/" + URLEncoder.encode( parts[i] );
307         }
308 
309         //Parent directories need to be created before posting
310         try
311         {
312             mkdirs( PathUtils.dirname( resource.getName() ) );
313         }
314         catch ( IOException e )
315         {
316             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
317         }
318 
319         PutMethod putMethod = new PutMethod( url );
320 
321         firePutStarted( resource, source );
322                 
323         try
324         {
325             putMethod.setRequestEntity( new RequestEntityImplementation( stream, resource, this, source ) );
326 
327             int statusCode;
328             try
329             {
330                 statusCode = execute( putMethod );
331             }
332             catch ( IOException e )
333             {
334                 fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
335 
336                 throw new TransferFailedException( e.getMessage(), e );
337             }
338 
339             fireTransferDebug( url + " - Status code: " + statusCode );
340 
341             // Check that we didn't run out of retries.
342             switch ( statusCode )
343             {
344                 // Success Codes
345                 case HttpStatus.SC_OK: // 200
346                 case HttpStatus.SC_CREATED: // 201
347                 case HttpStatus.SC_ACCEPTED: // 202
348                 case HttpStatus.SC_NO_CONTENT:  // 204
349                     break;
350 
351                 case SC_NULL:
352                 {
353                     TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
354                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
355                     throw e;
356                 }
357 
358                 case HttpStatus.SC_FORBIDDEN:
359                     fireSessionConnectionRefused();
360                     throw new AuthorizationException( "Access denied to: " + url );
361 
362                 case HttpStatus.SC_NOT_FOUND:
363                     throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
364 
365                 //add more entries here
366                 default :
367                 {
368                     TransferFailedException e =
369                             new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
370                                 + statusCode );
371                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
372                     throw e;
373                 }
374             }
375 
376             firePutCompleted( resource, source );
377         }
378         finally
379         {
380             putMethod.releaseConnection();
381         }
382     }
383     
384     protected void mkdirs( String dirname ) throws HttpException, IOException
385     {
386     }
387 
388     public boolean resourceExists( String resourceName )
389         throws TransferFailedException, AuthorizationException
390     {
391         String url = getRepository().getUrl() + "/" + resourceName;
392         HeadMethod headMethod = new HeadMethod( url );
393         int statusCode;
394         try
395         {
396             statusCode = execute( headMethod );
397         }
398         catch ( IOException e )
399         {
400             throw new TransferFailedException( e.getMessage(), e );
401         }
402         try
403         {
404             switch ( statusCode )
405             {
406                 case HttpStatus.SC_OK:
407                     return true;
408 
409                 case HttpStatus.SC_NOT_MODIFIED:
410                     return true;
411 
412                 case SC_NULL:
413                     throw new TransferFailedException( "Failed to transfer file: " + url );
414 
415                 case HttpStatus.SC_FORBIDDEN:
416                     throw new AuthorizationException( "Access denied to: " + url );
417 
418                 case HttpStatus.SC_UNAUTHORIZED:
419                     throw new AuthorizationException( "Not authorized." );
420 
421                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
422                     throw new AuthorizationException( "Not authorized by proxy." );
423 
424                 case HttpStatus.SC_NOT_FOUND:
425                     return false;
426 
427                     //add more entries here
428                 default:
429                     throw new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
430                         + statusCode );
431             }
432         }
433         finally
434         {
435             headMethod.releaseConnection();
436         }
437     }
438 
439     protected int execute( HttpMethod httpMethod ) throws HttpException, IOException
440     {
441         int statusCode = SC_NULL;
442         
443         setParameters( httpMethod );
444         setHeaders( httpMethod );
445         
446         statusCode = client.executeMethod( httpMethod );
447         return statusCode;
448     }
449     
450     protected void setParameters( HttpMethod method )
451     {
452         HttpMethodConfiguration config = httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
453         if ( config != null )
454         {
455             HttpMethodParams params = config.asMethodParams( method.getParams() );
456             if ( params != null )
457             {
458                 method.setParams( params );
459             }
460         }
461         
462         if ( config == null || config.getConnectionTimeout() == HttpMethodConfiguration.DEFAULT_CONNECTION_TIMEOUT )
463         {
464             method.getParams().setSoTimeout( getTimeout() );
465         }
466     }
467 
468     protected void setHeaders( HttpMethod method )
469     {
470         HttpMethodConfiguration config = httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
471         if ( config == null || config.isUseDefaultHeaders() )
472         {
473             // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
474             method.addRequestHeader( "Cache-control", "no-cache" );
475             method.addRequestHeader( "Cache-store", "no-store" );
476             method.addRequestHeader( "Pragma", "no-cache" );
477             method.addRequestHeader( "Expires", "0" );
478             method.addRequestHeader( "Accept-Encoding", "gzip" );
479         }
480 
481         if ( httpHeaders != null )
482         {
483             for ( Iterator i = httpHeaders.keySet().iterator(); i.hasNext(); )
484             {
485                 String header = (String) i.next();
486                 method.addRequestHeader( header, httpHeaders.getProperty( header ) );
487             }                
488         }
489         
490         Header[] headers = config == null ? null : config.asRequestHeaders();
491         if ( headers != null )
492         {
493             for ( int i = 0; i < headers.length; i++ )
494             {
495                 method.addRequestHeader( headers[i] );
496             }
497         }
498     }
499 
500     /**
501      * getUrl
502      * Implementors can override this to remove unwanted parts of the url such as role-hints
503      * @param repository
504      * @return
505      */
506     protected String getURL( Repository repository )
507     {
508         return repository.getUrl();
509     }
510 
511     protected HttpClient getClient()
512     {
513         return client;
514     }
515 
516     public void setConnectionManager( HttpConnectionManager connectionManager )
517     {
518         this.connectionManager = connectionManager;
519     }
520 
521     public Properties getHttpHeaders()
522     {
523         return httpHeaders;
524     }
525 
526     public void setHttpHeaders( Properties httpHeaders )
527     {
528         this.httpHeaders = httpHeaders;
529     }
530 
531     public HttpConfiguration getHttpConfiguration()
532     {
533         return httpConfiguration;
534     }
535 
536     public void setHttpConfiguration( HttpConfiguration httpConfiguration )
537     {
538         this.httpConfiguration = httpConfiguration;
539     }
540 
541     public void fillInputData( InputData inputData )
542         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
543     {
544         Resource resource = inputData.getResource();
545         
546         String url = getRepository().getUrl() + "/" + resource.getName();
547         getMethod = new GetMethod( url );
548         long timestamp = resource.getLastModified();
549         if ( timestamp > 0 )
550         {
551             SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
552             fmt.setTimeZone( GMT_TIME_ZONE );
553             Header hdr = new Header( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
554             fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
555             getMethod.addRequestHeader( hdr );
556         }
557 
558         int statusCode;
559         try
560         {
561             statusCode = execute( getMethod );
562         }
563         catch ( IOException e )
564         {
565             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
566 
567             throw new TransferFailedException( e.getMessage(), e );
568         }
569 
570         fireTransferDebug( url + " - Status code: " + statusCode );
571 
572         // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is
573         // required
574         switch ( statusCode )
575         {
576             case HttpStatus.SC_OK:
577                 break;
578 
579             case HttpStatus.SC_NOT_MODIFIED:
580                 // return, leaving last modified set to original value so getIfNewer should return unmodified
581                 return;
582 
583             case SC_NULL:
584             {
585                 TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
586                 fireTransferError( resource, e, TransferEvent.REQUEST_GET );
587                 throw e;
588             }
589 
590             case HttpStatus.SC_FORBIDDEN:
591                 fireSessionConnectionRefused();
592                 throw new AuthorizationException( "Access denied to: " + url );
593 
594             case HttpStatus.SC_UNAUTHORIZED:
595                 fireSessionConnectionRefused();
596                 throw new AuthorizationException( "Not authorized." );
597 
598             case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
599                 fireSessionConnectionRefused();
600                 throw new AuthorizationException( "Not authorized by proxy." );
601 
602             case HttpStatus.SC_NOT_FOUND:
603                 throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
604 
605                 // add more entries here
606             default:
607             {
608                 cleanupGetTransfer( resource );
609                 TransferFailedException e =
610                     new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
611                         + statusCode );
612                 fireTransferError( resource, e, TransferEvent.REQUEST_GET );
613                 throw e;
614             }
615         }
616 
617         InputStream is = null;
618 
619         Header contentLengthHeader = getMethod.getResponseHeader( "Content-Length" );
620 
621         if ( contentLengthHeader != null )
622         {
623             try
624             {
625                 long contentLength = Integer.valueOf( contentLengthHeader.getValue() ).intValue();
626 
627                 resource.setContentLength( contentLength );
628             }
629             catch ( NumberFormatException e )
630             {
631                 fireTransferDebug( "error parsing content length header '" + contentLengthHeader.getValue() + "' "
632                     + e );
633             }
634         }
635 
636         Header lastModifiedHeader = getMethod.getResponseHeader( "Last-Modified" );
637 
638         long lastModified = 0;
639 
640         if ( lastModifiedHeader != null )
641         {
642             try
643             {
644                 lastModified = DateUtil.parseDate( lastModifiedHeader.getValue() ).getTime();
645 
646                 resource.setLastModified( lastModified );
647             }
648             catch ( DateParseException e )
649             {
650                 fireTransferDebug( "Unable to parse last modified header" );
651             }
652 
653             fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified + ")" );
654         }
655 
656         Header contentEncoding = getMethod.getResponseHeader( "Content-Encoding" );
657         boolean isGZipped =
658             contentEncoding == null ? false : "gzip".equalsIgnoreCase( contentEncoding.getValue() );
659 
660         try
661         {
662             is = getMethod.getResponseBodyAsStream();
663             if ( isGZipped )
664             {
665                 is = new GZIPInputStream( is );
666             }
667         }
668         catch ( IOException e )
669         {
670             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
671 
672             String msg =
673                 "Error occurred while retrieving from remote repository:" + getRepository() + ": " + e.getMessage();
674             
675             throw new TransferFailedException( msg, e );
676         }
677         
678         inputData.setInputStream( is );
679     }
680 
681     protected void cleanupGetTransfer( Resource resource )
682     {
683         if ( getMethod != null )
684         {
685             getMethod.releaseConnection();
686         }
687     }
688 
689     public void fillOutputData( OutputData outputData )
690         throws TransferFailedException
691     {
692         throw new IllegalStateException( "Should not be using the streaming wagon for HTTP PUT" );        
693     }
694 }