View Javadoc
1   package org.eclipse.aether.transport.wagon;
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 java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Properties;
33  import java.util.Queue;
34  import java.util.UUID;
35  import java.util.concurrent.ConcurrentLinkedQueue;
36  import java.util.concurrent.atomic.AtomicBoolean;
37  
38  import org.apache.maven.wagon.ConnectionException;
39  import org.apache.maven.wagon.ResourceDoesNotExistException;
40  import org.apache.maven.wagon.StreamingWagon;
41  import org.apache.maven.wagon.Wagon;
42  import org.apache.maven.wagon.WagonException;
43  import org.apache.maven.wagon.authentication.AuthenticationInfo;
44  import org.apache.maven.wagon.proxy.ProxyInfo;
45  import org.apache.maven.wagon.proxy.ProxyInfoProvider;
46  import org.apache.maven.wagon.repository.Repository;
47  import org.apache.maven.wagon.repository.RepositoryPermissions;
48  import org.eclipse.aether.ConfigurationProperties;
49  import org.eclipse.aether.RepositorySystemSession;
50  import org.eclipse.aether.repository.AuthenticationContext;
51  import org.eclipse.aether.repository.Proxy;
52  import org.eclipse.aether.repository.RemoteRepository;
53  import org.eclipse.aether.spi.connector.transport.GetTask;
54  import org.eclipse.aether.spi.connector.transport.PeekTask;
55  import org.eclipse.aether.spi.connector.transport.PutTask;
56  import org.eclipse.aether.spi.connector.transport.TransportTask;
57  import org.eclipse.aether.spi.connector.transport.Transporter;
58  import org.eclipse.aether.transfer.NoTransporterException;
59  import org.eclipse.aether.util.ConfigUtils;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * A transporter using Maven Wagon.
65   */
66  final class WagonTransporter
67      implements Transporter
68  {
69  
70      private static final String CONFIG_PROP_CONFIG = "aether.connector.wagon.config";
71  
72      private static final String CONFIG_PROP_FILE_MODE = "aether.connector.perms.fileMode";
73  
74      private static final String CONFIG_PROP_DIR_MODE = "aether.connector.perms.dirMode";
75  
76      private static final String CONFIG_PROP_GROUP = "aether.connector.perms.group";
77  
78      private static final Logger LOGGER = LoggerFactory.getLogger( WagonTransporter.class );
79  
80      private final RemoteRepository repository;
81  
82      private final RepositorySystemSession session;
83  
84      private final AuthenticationContext repoAuthContext;
85  
86      private final AuthenticationContext proxyAuthContext;
87  
88      private final WagonProvider wagonProvider;
89  
90      private final WagonConfigurator wagonConfigurator;
91  
92      private final String wagonHint;
93  
94      private final Repository wagonRepo;
95  
96      private final AuthenticationInfo wagonAuth;
97  
98      private final ProxyInfoProvider wagonProxy;
99  
100     private final Properties headers;
101 
102     private final Queue<Wagon> wagons = new ConcurrentLinkedQueue<>();
103 
104     private final AtomicBoolean closed = new AtomicBoolean();
105 
106     WagonTransporter( WagonProvider wagonProvider, WagonConfigurator wagonConfigurator,
107                              RemoteRepository repository, RepositorySystemSession session )
108         throws NoTransporterException
109     {
110         this.wagonProvider = wagonProvider;
111         this.wagonConfigurator = wagonConfigurator;
112         this.repository = repository;
113         this.session = session;
114 
115         wagonRepo = new Repository( repository.getId(), repository.getUrl() );
116         wagonRepo.setPermissions( getPermissions( repository.getId(), session ) );
117 
118         wagonHint = wagonRepo.getProtocol().toLowerCase( Locale.ENGLISH );
119         if ( wagonHint == null || wagonHint.length() <= 0 )
120         {
121             throw new NoTransporterException( repository );
122         }
123 
124         try
125         {
126             wagons.add( lookupWagon() );
127         }
128         catch ( Exception e )
129         {
130             LOGGER.debug( "No transport {}", e );
131             throw new NoTransporterException( repository, e );
132         }
133 
134         repoAuthContext = AuthenticationContext.forRepository( session, repository );
135         proxyAuthContext = AuthenticationContext.forProxy( session, repository );
136 
137         wagonAuth = getAuthenticationInfo( repoAuthContext );
138         wagonProxy = getProxy( repository, proxyAuthContext );
139 
140         headers = new Properties();
141         headers.put( "User-Agent", ConfigUtils.getString( session, ConfigurationProperties.DEFAULT_USER_AGENT,
142                                                           ConfigurationProperties.USER_AGENT ) );
143         Map<?, ?> headers =
144             ConfigUtils.getMap( session, null, ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
145                                 ConfigurationProperties.HTTP_HEADERS );
146         if ( headers != null )
147         {
148             this.headers.putAll( headers );
149         }
150     }
151 
152     private static RepositoryPermissions getPermissions( String repoId, RepositorySystemSession session )
153     {
154         RepositoryPermissions result = null;
155 
156         RepositoryPermissions perms = new RepositoryPermissions();
157 
158         String suffix = '.' + repoId;
159 
160         String fileMode = ConfigUtils.getString( session, null, CONFIG_PROP_FILE_MODE + suffix );
161         if ( fileMode != null )
162         {
163             perms.setFileMode( fileMode );
164             result = perms;
165         }
166 
167         String dirMode = ConfigUtils.getString( session, null, CONFIG_PROP_DIR_MODE + suffix );
168         if ( dirMode != null )
169         {
170             perms.setDirectoryMode( dirMode );
171             result = perms;
172         }
173 
174         String group = ConfigUtils.getString( session, null, CONFIG_PROP_GROUP + suffix );
175         if ( group != null )
176         {
177             perms.setGroup( group );
178             result = perms;
179         }
180 
181         return result;
182     }
183 
184     private AuthenticationInfo getAuthenticationInfo( final AuthenticationContext authContext )
185     {
186         AuthenticationInfo auth = null;
187 
188         if ( authContext != null )
189         {
190             auth = new AuthenticationInfo()
191             {
192                 @Override
193                 public String getUserName()
194                 {
195                     return authContext.get( AuthenticationContext.USERNAME );
196                 }
197 
198                 @Override
199                 public String getPassword()
200                 {
201                     return authContext.get( AuthenticationContext.PASSWORD );
202                 }
203 
204                 @Override
205                 public String getPrivateKey()
206                 {
207                     return authContext.get( AuthenticationContext.PRIVATE_KEY_PATH );
208                 }
209 
210                 @Override
211                 public String getPassphrase()
212                 {
213                     return authContext.get( AuthenticationContext.PRIVATE_KEY_PASSPHRASE );
214                 }
215             };
216         }
217 
218         return auth;
219     }
220 
221     private ProxyInfoProvider getProxy( RemoteRepository repository, final AuthenticationContext authContext )
222     {
223         ProxyInfoProvider proxy = null;
224 
225         Proxy p = repository.getProxy();
226         if ( p != null )
227         {
228             final ProxyInfo prox;
229             if ( authContext != null )
230             {
231                 prox = new ProxyInfo()
232                 {
233                     @Override
234                     public String getUserName()
235                     {
236                         return authContext.get( AuthenticationContext.USERNAME );
237                     }
238 
239                     @Override
240                     public String getPassword()
241                     {
242                         return authContext.get( AuthenticationContext.PASSWORD );
243                     }
244 
245                     @Override
246                     public String getNtlmDomain()
247                     {
248                         return authContext.get( AuthenticationContext.NTLM_DOMAIN );
249                     }
250 
251                     @Override
252                     public String getNtlmHost()
253                     {
254                         return authContext.get( AuthenticationContext.NTLM_WORKSTATION );
255                     }
256                 };
257             }
258             else
259             {
260                 prox = new ProxyInfo();
261             }
262             prox.setType( p.getType() );
263             prox.setHost( p.getHost() );
264             prox.setPort( p.getPort() );
265 
266             proxy = new ProxyInfoProvider()
267             {
268                 @Override
269                 public ProxyInfo getProxyInfo( String protocol )
270                 {
271                     return prox;
272                 }
273             };
274         }
275 
276         return proxy;
277     }
278 
279     private Wagon lookupWagon()
280         throws Exception
281     {
282         return wagonProvider.lookup( wagonHint );
283     }
284 
285     private void releaseWagon( Wagon wagon )
286     {
287         wagonProvider.release( wagon );
288     }
289 
290     private void connectWagon( Wagon wagon )
291         throws WagonException
292     {
293         if ( !headers.isEmpty() )
294         {
295             try
296             {
297                 Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class );
298                 setHttpHeaders.invoke( wagon, headers );
299             }
300             catch ( NoSuchMethodException e )
301             {
302                 // normal for non-http wagons
303             }
304             catch ( InvocationTargetException | IllegalAccessException | RuntimeException e )
305             {
306                 LOGGER.debug( "Could not set user agent for Wagon {}", wagon.getClass().getName(), e );
307             }
308         }
309 
310         int connectTimeout =
311             ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
312                                     ConfigurationProperties.CONNECT_TIMEOUT );
313         int requestTimeout =
314             ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
315                                     ConfigurationProperties.REQUEST_TIMEOUT );
316 
317         wagon.setTimeout( Math.max( Math.max( connectTimeout, requestTimeout ), 0 ) );
318 
319         wagon.setInteractive( ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_INTERACTIVE,
320                                                       ConfigurationProperties.INTERACTIVE ) );
321 
322         Object configuration = ConfigUtils.getObject( session, null, CONFIG_PROP_CONFIG + "." + repository.getId() );
323         if ( configuration != null && wagonConfigurator != null )
324         {
325             try
326             {
327                 wagonConfigurator.configure( wagon, configuration );
328             }
329             catch ( Exception e )
330             {
331                 LOGGER.warn( "Could not apply configuration for {} to Wagon {}",
332                         repository.getId(), wagon.getClass().getName(), e );
333             }
334         }
335 
336         wagon.connect( wagonRepo, wagonAuth, wagonProxy );
337     }
338 
339     private void disconnectWagon( Wagon wagon )
340     {
341         try
342         {
343             if ( wagon != null )
344             {
345                 wagon.disconnect();
346             }
347         }
348         catch ( ConnectionException e )
349         {
350             LOGGER.debug( "Could not disconnect Wagon {}", wagon, e );
351         }
352     }
353 
354     private Wagon pollWagon()
355         throws Exception
356     {
357         Wagon wagon = wagons.poll();
358 
359         if ( wagon == null )
360         {
361             try
362             {
363                 wagon = lookupWagon();
364                 connectWagon( wagon );
365             }
366             catch ( Exception e )
367             {
368                 releaseWagon( wagon );
369                 throw e;
370             }
371         }
372         else if ( wagon.getRepository() == null )
373         {
374             try
375             {
376                 connectWagon( wagon );
377             }
378             catch ( Exception e )
379             {
380                 wagons.add( wagon );
381                 throw e;
382             }
383         }
384 
385         return wagon;
386     }
387 
388     public int classify( Throwable error )
389     {
390         if ( error instanceof ResourceDoesNotExistException )
391         {
392             return ERROR_NOT_FOUND;
393         }
394         return ERROR_OTHER;
395     }
396 
397     public void peek( PeekTask task )
398         throws Exception
399     {
400         execute( task, new PeekTaskRunner( task ) );
401     }
402 
403     public void get( GetTask task )
404         throws Exception
405     {
406         execute( task, new GetTaskRunner( task ) );
407     }
408 
409     public void put( PutTask task )
410         throws Exception
411     {
412         execute( task, new PutTaskRunner( task ) );
413     }
414 
415     private void execute( TransportTask task, TaskRunner runner )
416         throws Exception
417     {
418         if ( closed.get() )
419         {
420             throw new IllegalStateException( "transporter closed, cannot execute task " + task );
421         }
422         try
423         {
424             WagonTransferListener listener = new WagonTransferListener( task.getListener() );
425             Wagon wagon = pollWagon();
426             try
427             {
428                 wagon.addTransferListener( listener );
429                 runner.run( wagon );
430             }
431             finally
432             {
433                 wagon.removeTransferListener( listener );
434                 wagons.add( wagon );
435             }
436         }
437         catch ( RuntimeException e )
438         {
439             throw WagonCancelledException.unwrap( e );
440         }
441     }
442 
443     private static File newTempFile()
444         throws IOException
445     {
446         return File.createTempFile( "wagon-" + UUID.randomUUID().toString().replace( "-", "" ), ".tmp" );
447     }
448 
449     private void delTempFile( File path )
450     {
451         if ( path != null && !path.delete() && path.exists() )
452         {
453             LOGGER.debug( "Could not delete temporary file {}", path );
454             path.deleteOnExit();
455         }
456     }
457 
458     private static void copy( OutputStream os, InputStream is )
459         throws IOException
460     {
461         byte[] buffer = new byte[1024 * 32];
462         for ( int read = is.read( buffer ); read >= 0; read = is.read( buffer ) )
463         {
464             os.write( buffer, 0, read );
465         }
466     }
467 
468     public void close()
469     {
470         if ( closed.compareAndSet( false, true ) )
471         {
472             AuthenticationContext.close( repoAuthContext );
473             AuthenticationContext.close( proxyAuthContext );
474 
475             for ( Wagon wagon = wagons.poll(); wagon != null; wagon = wagons.poll() )
476             {
477                 disconnectWagon( wagon );
478                 releaseWagon( wagon );
479             }
480         }
481     }
482 
483     private interface TaskRunner
484     {
485 
486         void run( Wagon wagon )
487             throws IOException, WagonException;
488 
489     }
490 
491     private static class PeekTaskRunner
492         implements TaskRunner
493     {
494 
495         private final PeekTask task;
496 
497         PeekTaskRunner( PeekTask task )
498         {
499             this.task = task;
500         }
501 
502         @Override
503         public void run( Wagon wagon )
504             throws WagonException
505         {
506             String src = task.getLocation().toString();
507             if ( !wagon.resourceExists( src ) )
508             {
509                 throw new ResourceDoesNotExistException( "Could not find " + src + " in "
510                     + wagon.getRepository().getUrl() );
511             }
512         }
513 
514     }
515 
516     private class GetTaskRunner
517         implements TaskRunner
518     {
519 
520         private final GetTask task;
521 
522         GetTaskRunner( GetTask task )
523         {
524             this.task = task;
525         }
526 
527         @Override
528         public void run( Wagon wagon )
529             throws IOException, WagonException
530         {
531             String src = task.getLocation().toString();
532             File file = task.getDataFile();
533             if ( file == null && wagon instanceof StreamingWagon )
534             {
535                 try ( OutputStream dst = task.newOutputStream() )
536                 {
537                     ( (StreamingWagon) wagon ).getToStream( src, dst );
538                 }
539             }
540             else
541             {
542                 File dst = ( file != null ) ? file : newTempFile();
543                 try
544                 {
545                     wagon.get( src, dst );
546                     /*
547                      * NOTE: Wagon (1.0-beta-6) doesn't create the destination file when transferring a 0-byte
548                      * resource. So if the resource we asked for didn't cause any exception but doesn't show up in
549                      * the dst file either, Wagon tells us in its weird way the file is empty.
550                      */
551                     if ( !dst.exists() && !dst.createNewFile() )
552                     {
553                         throw new IOException( String.format( "Failure creating file '%s'.", dst.getAbsolutePath() ) );
554                     }
555                     if ( file == null )
556                     {
557                         readTempFile( dst );
558                     }
559                 }
560                 finally
561                 {
562                     if ( file == null )
563                     {
564                         delTempFile( dst );
565                     }
566                 }
567             }
568         }
569 
570         private void readTempFile( File dst )
571             throws IOException
572         {
573             try ( FileInputStream in = new FileInputStream( dst );
574                     OutputStream out = task.newOutputStream() )
575             {
576                 copy( out, in );
577             }
578         }
579 
580     }
581 
582     private class PutTaskRunner
583         implements TaskRunner
584     {
585 
586         private final PutTask task;
587 
588         PutTaskRunner( PutTask task )
589         {
590             this.task = task;
591         }
592 
593         @Override
594         public void run( Wagon wagon )
595             throws WagonException, IOException
596         {
597             String dst = task.getLocation().toString();
598             File file = task.getDataFile();
599             if ( file == null && wagon instanceof StreamingWagon )
600             {
601                 try ( InputStream src = task.newInputStream() )
602                 {
603                     // StreamingWagon uses an internal buffer on src input stream.
604                     ( (StreamingWagon) wagon ).putFromStream( src, dst, task.getDataLength(), -1 );
605                 }
606             }
607             else
608             {
609                 File src = ( file != null ) ? file : createTempFile();
610                 try
611                 {
612                     wagon.put( src, dst );
613                 }
614                 finally
615                 {
616                     if ( file == null )
617                     {
618                         delTempFile( src );
619                     }
620                 }
621             }
622         }
623 
624         private File createTempFile()
625             throws IOException
626         {
627             File tmp = newTempFile();
628             
629             try ( InputStream in = task.newInputStream();
630                     OutputStream out = new FileOutputStream( tmp ) ) 
631             {
632                 copy( out, in );
633             }
634             catch ( IOException e )
635             {
636                 delTempFile( tmp );
637                 throw e;
638             }
639 
640             return tmp;
641         }
642 
643     }
644 
645 }