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( repository, 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( RemoteRepository repository,
185                                                       final AuthenticationContext authContext )
186     {
187         AuthenticationInfo auth = null;
188 
189         if ( authContext != null )
190         {
191             auth = new AuthenticationInfo()
192             {
193                 @Override
194                 public String getUserName()
195                 {
196                     return authContext.get( AuthenticationContext.USERNAME );
197                 }
198 
199                 @Override
200                 public String getPassword()
201                 {
202                     return authContext.get( AuthenticationContext.PASSWORD );
203                 }
204 
205                 @Override
206                 public String getPrivateKey()
207                 {
208                     return authContext.get( AuthenticationContext.PRIVATE_KEY_PATH );
209                 }
210 
211                 @Override
212                 public String getPassphrase()
213                 {
214                     return authContext.get( AuthenticationContext.PRIVATE_KEY_PASSPHRASE );
215                 }
216             };
217         }
218 
219         return auth;
220     }
221 
222     private ProxyInfoProvider getProxy( RemoteRepository repository, final AuthenticationContext authContext )
223     {
224         ProxyInfoProvider proxy = null;
225 
226         Proxy p = repository.getProxy();
227         if ( p != null )
228         {
229             final ProxyInfo prox;
230             if ( authContext != null )
231             {
232                 prox = new ProxyInfo()
233                 {
234                     @Override
235                     public String getUserName()
236                     {
237                         return authContext.get( AuthenticationContext.USERNAME );
238                     }
239 
240                     @Override
241                     public String getPassword()
242                     {
243                         return authContext.get( AuthenticationContext.PASSWORD );
244                     }
245 
246                     @Override
247                     public String getNtlmDomain()
248                     {
249                         return authContext.get( AuthenticationContext.NTLM_DOMAIN );
250                     }
251 
252                     @Override
253                     public String getNtlmHost()
254                     {
255                         return authContext.get( AuthenticationContext.NTLM_WORKSTATION );
256                     }
257                 };
258             }
259             else
260             {
261                 prox = new ProxyInfo();
262             }
263             prox.setType( p.getType() );
264             prox.setHost( p.getHost() );
265             prox.setPort( p.getPort() );
266 
267             proxy = new ProxyInfoProvider()
268             {
269                 @Override
270                 public ProxyInfo getProxyInfo( String protocol )
271                 {
272                     return prox;
273                 }
274             };
275         }
276 
277         return proxy;
278     }
279 
280     private Wagon lookupWagon()
281         throws Exception
282     {
283         return wagonProvider.lookup( wagonHint );
284     }
285 
286     private void releaseWagon( Wagon wagon )
287     {
288         wagonProvider.release( wagon );
289     }
290 
291     private void connectWagon( Wagon wagon )
292         throws WagonException
293     {
294         if ( !headers.isEmpty() )
295         {
296             try
297             {
298                 Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class );
299                 setHttpHeaders.invoke( wagon, headers );
300             }
301             catch ( NoSuchMethodException e )
302             {
303                 // normal for non-http wagons
304             }
305             catch ( InvocationTargetException | IllegalAccessException | RuntimeException e )
306             {
307                 LOGGER.debug( "Could not set user agent for Wagon {}", wagon.getClass().getName(), e );
308             }
309         }
310 
311         int connectTimeout =
312             ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
313                                     ConfigurationProperties.CONNECT_TIMEOUT );
314         int requestTimeout =
315             ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
316                                     ConfigurationProperties.REQUEST_TIMEOUT );
317 
318         wagon.setTimeout( Math.max( Math.max( connectTimeout, requestTimeout ), 0 ) );
319 
320         wagon.setInteractive( ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_INTERACTIVE,
321                                                       ConfigurationProperties.INTERACTIVE ) );
322 
323         Object configuration = ConfigUtils.getObject( session, null, CONFIG_PROP_CONFIG + "." + repository.getId() );
324         if ( configuration != null && wagonConfigurator != null )
325         {
326             try
327             {
328                 wagonConfigurator.configure( wagon, configuration );
329             }
330             catch ( Exception e )
331             {
332                 LOGGER.warn( "Could not apply configuration for {} to Wagon {}",
333                         repository.getId(), wagon.getClass().getName(), e );
334             }
335         }
336 
337         wagon.connect( wagonRepo, wagonAuth, wagonProxy );
338     }
339 
340     private void disconnectWagon( Wagon wagon )
341     {
342         try
343         {
344             if ( wagon != null )
345             {
346                 wagon.disconnect();
347             }
348         }
349         catch ( ConnectionException e )
350         {
351             LOGGER.debug( "Could not disconnect Wagon {}", wagon, e );
352         }
353     }
354 
355     private Wagon pollWagon()
356         throws Exception
357     {
358         Wagon wagon = wagons.poll();
359 
360         if ( wagon == null )
361         {
362             try
363             {
364                 wagon = lookupWagon();
365                 connectWagon( wagon );
366             }
367             catch ( Exception e )
368             {
369                 releaseWagon( wagon );
370                 throw e;
371             }
372         }
373         else if ( wagon.getRepository() == null )
374         {
375             try
376             {
377                 connectWagon( wagon );
378             }
379             catch ( Exception e )
380             {
381                 wagons.add( wagon );
382                 throw e;
383             }
384         }
385 
386         return wagon;
387     }
388 
389     public int classify( Throwable error )
390     {
391         if ( error instanceof ResourceDoesNotExistException )
392         {
393             return ERROR_NOT_FOUND;
394         }
395         return ERROR_OTHER;
396     }
397 
398     public void peek( PeekTask task )
399         throws Exception
400     {
401         execute( task, new PeekTaskRunner( task ) );
402     }
403 
404     public void get( GetTask task )
405         throws Exception
406     {
407         execute( task, new GetTaskRunner( task ) );
408     }
409 
410     public void put( PutTask task )
411         throws Exception
412     {
413         execute( task, new PutTaskRunner( task ) );
414     }
415 
416     private void execute( TransportTask task, TaskRunner runner )
417         throws Exception
418     {
419         if ( closed.get() )
420         {
421             throw new IllegalStateException( "transporter closed, cannot execute task " + task );
422         }
423         try
424         {
425             WagonTransferListener listener = new WagonTransferListener( task.getListener() );
426             Wagon wagon = pollWagon();
427             try
428             {
429                 wagon.addTransferListener( listener );
430                 runner.run( wagon );
431             }
432             finally
433             {
434                 wagon.removeTransferListener( listener );
435                 wagons.add( wagon );
436             }
437         }
438         catch ( RuntimeException e )
439         {
440             throw WagonCancelledException.unwrap( e );
441         }
442     }
443 
444     private static File newTempFile()
445         throws IOException
446     {
447         return File.createTempFile( "wagon-" + UUID.randomUUID().toString().replace( "-", "" ), ".tmp" );
448     }
449 
450     private void delTempFile( File path )
451     {
452         if ( path != null && !path.delete() && path.exists() )
453         {
454             LOGGER.debug( "Could not delete temporary file {}", path );
455             path.deleteOnExit();
456         }
457     }
458 
459     private static void copy( OutputStream os, InputStream is )
460         throws IOException
461     {
462         byte[] buffer = new byte[1024 * 32];
463         for ( int read = is.read( buffer ); read >= 0; read = is.read( buffer ) )
464         {
465             os.write( buffer, 0, read );
466         }
467     }
468 
469     public void close()
470     {
471         if ( closed.compareAndSet( false, true ) )
472         {
473             AuthenticationContext.close( repoAuthContext );
474             AuthenticationContext.close( proxyAuthContext );
475 
476             for ( Wagon wagon = wagons.poll(); wagon != null; wagon = wagons.poll() )
477             {
478                 disconnectWagon( wagon );
479                 releaseWagon( wagon );
480             }
481         }
482     }
483 
484     private interface TaskRunner
485     {
486 
487         void run( Wagon wagon )
488             throws IOException, WagonException;
489 
490     }
491 
492     private static class PeekTaskRunner
493         implements TaskRunner
494     {
495 
496         private final PeekTask task;
497 
498         PeekTaskRunner( PeekTask task )
499         {
500             this.task = task;
501         }
502 
503         @Override
504         public void run( Wagon wagon )
505             throws WagonException
506         {
507             String src = task.getLocation().toString();
508             if ( !wagon.resourceExists( src ) )
509             {
510                 throw new ResourceDoesNotExistException( "Could not find " + src + " in "
511                     + wagon.getRepository().getUrl() );
512             }
513         }
514 
515     }
516 
517     private class GetTaskRunner
518         implements TaskRunner
519     {
520 
521         private final GetTask task;
522 
523         GetTaskRunner( GetTask task )
524         {
525             this.task = task;
526         }
527 
528         @Override
529         public void run( Wagon wagon )
530             throws IOException, WagonException
531         {
532             String src = task.getLocation().toString();
533             File file = task.getDataFile();
534             if ( file == null && wagon instanceof StreamingWagon )
535             {
536                 try ( OutputStream dst = task.newOutputStream() )
537                 {
538                     ( (StreamingWagon) wagon ).getToStream( src, dst );
539                 }
540             }
541             else
542             {
543                 File dst = ( file != null ) ? file : newTempFile();
544                 try
545                 {
546                     wagon.get( src, dst );
547                     /*
548                      * NOTE: Wagon (1.0-beta-6) doesn't create the destination file when transferring a 0-byte
549                      * resource. So if the resource we asked for didn't cause any exception but doesn't show up in
550                      * the dst file either, Wagon tells us in its weird way the file is empty.
551                      */
552                     if ( !dst.exists() && !dst.createNewFile() )
553                     {
554                         throw new IOException( String.format( "Failure creating file '%s'.", dst.getAbsolutePath() ) );
555                     }
556                     if ( file == null )
557                     {
558                         readTempFile( dst );
559                     }
560                 }
561                 finally
562                 {
563                     if ( file == null )
564                     {
565                         delTempFile( dst );
566                     }
567                 }
568             }
569         }
570 
571         private void readTempFile( File dst )
572             throws IOException
573         {
574             try ( FileInputStream in = new FileInputStream( dst );
575                     OutputStream out = task.newOutputStream() )
576             {
577                 copy( out, in );
578             }
579         }
580 
581     }
582 
583     private class PutTaskRunner
584         implements TaskRunner
585     {
586 
587         private final PutTask task;
588 
589         PutTaskRunner( PutTask task )
590         {
591             this.task = task;
592         }
593 
594         @Override
595         public void run( Wagon wagon )
596             throws WagonException, IOException
597         {
598             String dst = task.getLocation().toString();
599             File file = task.getDataFile();
600             if ( file == null && wagon instanceof StreamingWagon )
601             {
602                 try ( InputStream src = task.newInputStream() )
603                 {
604                     // StreamingWagon uses an internal buffer on src input stream.
605                     ( (StreamingWagon) wagon ).putFromStream( src, dst, task.getDataLength(), -1 );
606                 }
607             }
608             else
609             {
610                 File src = ( file != null ) ? file : createTempFile();
611                 try
612                 {
613                     wagon.put( src, dst );
614                 }
615                 finally
616                 {
617                     if ( file == null )
618                     {
619                         delTempFile( src );
620                     }
621                 }
622             }
623         }
624 
625         private File createTempFile()
626             throws IOException
627         {
628             File tmp = newTempFile();
629             
630             try ( InputStream in = task.newInputStream();
631                     OutputStream out = new FileOutputStream( tmp ) ) 
632             {
633                 copy( out, in );
634             }
635             catch ( IOException e )
636             {
637                 delTempFile( tmp );
638                 throw e;
639             }
640 
641             return tmp;
642         }
643 
644     }
645 
646 }