View Javadoc

1   package org.apache.maven.wagon.providers.scm;
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.maven.scm.ScmException;
23  import org.apache.maven.scm.ScmFile;
24  import org.apache.maven.scm.ScmFileSet;
25  import org.apache.maven.scm.ScmResult;
26  import org.apache.maven.scm.ScmVersion;
27  import org.apache.maven.scm.command.add.AddScmResult;
28  import org.apache.maven.scm.command.checkout.CheckOutScmResult;
29  import org.apache.maven.scm.command.list.ListScmResult;
30  import org.apache.maven.scm.manager.NoSuchScmProviderException;
31  import org.apache.maven.scm.manager.ScmManager;
32  import org.apache.maven.scm.provider.ScmProvider;
33  import org.apache.maven.scm.provider.ScmProviderRepository;
34  import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
35  import org.apache.maven.scm.repository.ScmRepository;
36  import org.apache.maven.scm.repository.ScmRepositoryException;
37  import org.apache.maven.wagon.AbstractWagon;
38  import org.apache.maven.wagon.ConnectionException;
39  import org.apache.maven.wagon.ResourceDoesNotExistException;
40  import org.apache.maven.wagon.TransferFailedException;
41  import org.apache.maven.wagon.authorization.AuthorizationException;
42  import org.apache.maven.wagon.events.TransferEvent;
43  import org.apache.maven.wagon.resource.Resource;
44  import org.codehaus.plexus.util.FileUtils;
45  import org.codehaus.plexus.util.StringUtils;
46  
47  import java.io.File;
48  import java.io.IOException;
49  import java.text.DecimalFormat;
50  import java.util.ArrayList;
51  import java.util.Iterator;
52  import java.util.List;
53  import java.util.Random;
54  import java.util.Stack;
55  
56  /**
57   * Wagon provider to get and put files from and to SCM systems, using Maven-SCM as underlying transport.
58   * <p/>
59   * TODO it probably creates problems if the same wagon is used in two different SCM protocols, as instance variables can
60   * keep incorrect state.
61   * TODO: For doing releases we either have to be able to add files with checking out the repository structure which may not be
62   * possible, or the checkout directory needs to be a constant. Doing releases won't scale if you have to checkout the
63   * whole repository structure in order to add 3 files.
64   *
65   * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="scm" instantiation-strategy="per-lookup"
66   *
67   * @author <a href="brett@apache.org">Brett Porter</a>
68   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
69   * @author <a href="carlos@apache.org">Carlos Sanchez</a>
70   * @author Jason van Zyl
71   * @version $Id: ScmWagon.java 894435 2009-12-29 16:38:38Z bentmann $
72   */
73  public class ScmWagon
74      extends AbstractWagon
75  {
76      /**
77       * @plexus.requirement
78       */
79      private ScmManager scmManager;
80  
81      private File checkoutDirectory;
82  
83      /**
84       * Get the {@link ScmManager} used in this Wagon
85       *
86       * @return the {@link ScmManager}
87       */
88      public ScmManager getScmManager()
89      {
90          return scmManager;
91      }
92  
93      /**
94       * Set the {@link ScmManager} used in this Wagon
95       *
96       * @param scmManager
97       */
98      public void setScmManager( ScmManager scmManager )
99      {
100         this.scmManager = scmManager;
101     }
102 
103     /**
104      * Get the directory where Wagon will checkout files from SCM. This directory will be deleted!
105      *
106      * @return directory
107      */
108     public File getCheckoutDirectory()
109     {
110         return checkoutDirectory;
111     }
112 
113     /**
114      * Set the directory where Wagon will checkout files from SCM. This directory will be deleted!
115      *
116      * @param checkoutDirectory
117      */
118     public void setCheckoutDirectory( File checkoutDirectory )
119     {
120         this.checkoutDirectory = checkoutDirectory;
121     }
122 
123     /**
124      * Convenience method to get the {@link ScmProvider} implementation to handle the provided SCM type
125      *
126      * @param scmType type of SCM, eg. <code>svn</code>, <code>cvs</code>
127      * @return the {@link ScmProvider} that will handle provided SCM type
128      * @throws NoSuchScmProviderException if there is no {@link ScmProvider} able to handle that SCM type
129      */
130     public ScmProvider getScmProvider( String scmType )
131         throws NoSuchScmProviderException
132     {
133         return getScmManager().getProviderByType( scmType );
134     }
135 
136     /**
137      * This will cleanup the checkout directory
138      */
139     public void openConnectionInternal()
140         throws ConnectionException
141     {
142         if ( checkoutDirectory == null )
143         {
144             checkoutDirectory = createCheckoutDirectory();
145         }
146 
147         if ( checkoutDirectory.exists() )
148         {
149             removeCheckoutDirectory();
150         }
151 
152         checkoutDirectory.mkdirs();
153     }
154 
155     private File createCheckoutDirectory()
156     {
157         File checkoutDirectory;
158 
159         DecimalFormat fmt = new DecimalFormat( "#####" );
160 
161         Random rand = new Random( System.currentTimeMillis() + Runtime.getRuntime().freeMemory() );
162 
163         synchronized ( rand )
164         {
165             do
166             {
167                 checkoutDirectory = new File( System.getProperty( "java.io.tmpdir" ),
168                                               "wagon-scm" + fmt.format( Math.abs( rand.nextInt() ) ) + ".checkout" );
169             }
170             while ( checkoutDirectory.exists() );
171         }
172 
173         return checkoutDirectory;
174     }
175 
176 
177     private void removeCheckoutDirectory()
178         throws ConnectionException
179     {
180         if( checkoutDirectory == null )
181         {
182             return; // Silently return.
183         }
184 
185         try
186         {
187             FileUtils.deleteDirectory( checkoutDirectory );
188         }
189         catch ( IOException e )
190         {
191             throw new ConnectionException( "Unable to cleanup checkout directory", e );
192         }
193     }
194 
195     private ScmRepository getScmRepository( String url )
196         throws ScmRepositoryException, NoSuchScmProviderException
197     {
198         String username = null;
199 
200         String password = null;
201 
202         String privateKey = null;
203 
204         String passphrase = null;
205 
206         if ( authenticationInfo != null )
207         {
208             username = authenticationInfo.getUserName();
209 
210             password = authenticationInfo.getPassword();
211 
212             privateKey = authenticationInfo.getPrivateKey();
213 
214             passphrase = authenticationInfo.getPassphrase();
215         }
216 
217         ScmRepository scmRepository = getScmManager().makeScmRepository( url );
218 
219         ScmProviderRepository providerRepository = scmRepository.getProviderRepository();
220 
221         if ( StringUtils.isNotEmpty( username ) )
222         {
223             providerRepository.setUser( username );
224         }
225 
226         if ( StringUtils.isNotEmpty( password ) )
227         {
228             providerRepository.setPassword( password );
229         }
230 
231         if ( providerRepository instanceof ScmProviderRepositoryWithHost )
232         {
233             ScmProviderRepositoryWithHost providerRepo = (ScmProviderRepositoryWithHost) providerRepository;
234 
235             if ( StringUtils.isNotEmpty( privateKey ) )
236             {
237                 providerRepo.setPrivateKey( privateKey );
238             }
239 
240             if ( StringUtils.isNotEmpty( passphrase ) )
241             {
242                 providerRepo.setPassphrase( passphrase );
243             }
244         }
245 
246         return scmRepository;
247     }
248 
249     public void put( File source, String targetName )
250         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
251     {
252         if ( source.isDirectory() )
253         {
254             throw new IllegalArgumentException( "Source is a directory: " + source );
255         }
256         putInternal( source, targetName );
257     }
258 
259     /**
260      * Puts both files and directories
261      *
262      * @param source
263      * @param targetName
264      * @throws TransferFailedException
265      */
266     private void putInternal( File source, String targetName )
267         throws TransferFailedException
268     {
269         Resource target = new Resource( targetName );
270 
271         firePutInitiated( target, source );
272 
273         try
274         {
275             ScmRepository scmRepository = getScmRepository( getRepository().getUrl() );
276 
277             target.setContentLength( source.length() );
278             target.setLastModified( source.lastModified() );
279             
280             firePutStarted( target, source );
281 
282             String msg = "Wagon: Adding " + source.getName() + " to repository";
283 
284             ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() );
285 
286             String checkoutTargetName = source.isDirectory() ? targetName : getDirname( targetName );
287             String relPath = checkOut( scmProvider, scmRepository, checkoutTargetName, target );
288 
289             File newCheckoutDirectory = new File( checkoutDirectory, relPath );
290 
291             File scmFile =
292                 new File( newCheckoutDirectory, source.isDirectory() ? "" : getFilename( targetName ) );
293 
294             boolean fileAlreadyInScm = scmFile.exists();
295 
296             if ( !scmFile.equals( source ) )
297             {
298                 if ( source.isDirectory() )
299                 {
300                     FileUtils.copyDirectoryStructure( source, scmFile );
301                 }
302                 else
303                 {
304                     FileUtils.copyFile( source, scmFile );
305                 }
306             }
307 
308             if ( !fileAlreadyInScm || scmFile.isDirectory() )
309             {
310                 int addedFiles = addFiles( scmProvider, scmRepository, newCheckoutDirectory,
311                                            source.isDirectory() ? "" : scmFile.getName() );
312 
313                 if ( !fileAlreadyInScm && addedFiles == 0 )
314                 {
315                     throw new ScmException(
316                         "Unable to add file to SCM: " + scmFile + "; see error messages above for more information" );
317                 }
318             }
319 
320             ScmResult result = scmProvider.checkIn( scmRepository, new ScmFileSet( checkoutDirectory ),
321                                                     (ScmVersion) null, msg );
322 
323             checkScmResult( result );
324         }
325         catch ( ScmException e )
326         {
327             fireTransferError( target, e, TransferEvent.REQUEST_GET );
328             
329             throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e );
330         }
331         catch ( IOException e )
332         {
333             fireTransferError( target, e, TransferEvent.REQUEST_GET );
334             
335             throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e );
336         }
337 
338         if ( source.isFile() )
339         {
340             postProcessListeners( target, source, TransferEvent.REQUEST_PUT );
341         }
342 
343         firePutCompleted( target, source );
344     }
345 
346     /**
347      * Returns the relative path to targetName in the checkout dir. If the targetName already exists in the scm, this
348      * will be the empty string.
349      *
350      * @param scmProvider
351      * @param scmRepository
352      * @param targetName
353      * @return
354      * @throws TransferFailedException
355      */
356     private String checkOut( ScmProvider scmProvider, ScmRepository scmRepository, String targetName,
357                              Resource resource )
358         throws TransferFailedException
359     {
360         checkoutDirectory = createCheckoutDirectory();
361 
362         Stack stack = new Stack();
363 
364         String target = targetName;
365 
366         // totally ignore scmRepository parent stuff since that is not supported by all scms.
367         // Instead, assume that that url exists. If not, then that's an error.
368         // Check whether targetName, which is a relative path into the scm, exists.
369         // If it doesn't, check the parent, etc.
370 
371         try
372         {
373             while ( target.length() > 0 && !scmProvider
374                 .list( scmRepository, new ScmFileSet( new File( "." ), new File( target ) ), false, (ScmVersion) null )
375                 .isSuccess() )
376             {
377                 stack.push( getFilename( target ) );
378                 target = getDirname( target );
379             }
380         }
381         catch ( ScmException e )
382         {
383             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
384             
385             throw new TransferFailedException( "Error listing repository: " + e.getMessage(), e );
386         }
387 
388         // ok, we've established that target exists, or is empty.
389         // Check the resource out; if it doesn't exist, that means we're in the svn repo url root,
390         // and the configuration is incorrect. We will not try repo.getParent since most scm's don't
391         // implement that.
392 
393         try
394         {
395             scmRepository = getScmRepository( getRepository().getUrl() + "/" + target.replace( '\\', '/' ) );
396 
397             CheckOutScmResult ret = scmProvider.checkOut( scmRepository,
398                                                           new ScmFileSet( new File( checkoutDirectory, "" ) ),
399                                                           (ScmVersion) null, false );
400 
401             checkScmResult( ret );
402         }
403         catch ( ScmException e )
404         {
405             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
406             
407             throw new TransferFailedException( "Error checking out: " + e.getMessage(), e );
408         }
409 
410         // now create the subdirs in target, if it's a parent of targetName
411 
412         String relPath = "";
413 
414         while ( !stack.isEmpty() )
415         {
416             String p = (String) stack.pop();
417             relPath += p + "/";
418 
419             File newDir = new File( checkoutDirectory, relPath );
420             if ( !newDir.mkdirs() )
421             {
422                 throw new TransferFailedException( "Failed to create directory " + newDir.getAbsolutePath()
423                     + "; parent should exist: " + checkoutDirectory );
424             }
425 
426             try
427             {
428                 addFiles( scmProvider, scmRepository, checkoutDirectory, relPath );
429             }
430             catch ( ScmException e )
431             {
432                 fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
433                 
434                 throw new TransferFailedException( "Failed to add directory " + newDir + " to working copy", e );
435             }
436         }
437 
438         return relPath;
439     }
440 
441     /**
442      * Add a file or directory to a SCM repository. If it's a directory all its contents are added recursively.
443      * <p/>
444      * TODO this is less than optimal, SCM API should provide a way to add a directory recursively
445      *
446      * @param scmProvider   SCM provider
447      * @param scmRepository SCM repository
448      * @param basedir       local directory corresponding to scmRepository
449      * @param scmFilePath   path of the file or directory to add, relative to basedir
450      * @return the number of files added.
451      * @throws ScmException
452      */
453     private int addFiles( ScmProvider scmProvider, ScmRepository scmRepository, File basedir, String scmFilePath )
454         throws ScmException
455     {
456         int addedFiles = 0;
457 
458         File scmFile = new File( basedir, scmFilePath );
459 
460         if ( scmFilePath.length() != 0 )
461         {
462             AddScmResult result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) );
463 
464             /*
465              * TODO dirty fix to work around files with property svn:eol-style=native if a file has that property, first
466              * time file is added it fails, second time it succeeds the solution is check if the scm provider is svn and
467              * unset that property when the SCM API allows it
468              */
469             if ( !result.isSuccess() )
470             {
471                 result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) );
472             }
473 
474             addedFiles = result.getAddedFiles().size();
475         }
476 
477         String reservedScmFile = scmProvider.getScmSpecificFilename();
478 
479         if ( scmFile.isDirectory() )
480         {
481             File[] files = scmFile.listFiles();
482 
483             for ( int i = 0; i < files.length; i++ )
484             {
485                 if ( reservedScmFile != null && !reservedScmFile.equals( files[i].getName() ) )
486                 {
487                     addedFiles += addFiles( scmProvider, scmRepository, basedir, ( scmFilePath.length() == 0 ? ""
488                         : scmFilePath + "/" ) + files[i].getName() );
489                 }
490             }
491         }
492 
493         return addedFiles;
494     }
495 
496     /**
497      * @return true
498      */
499     public boolean supportsDirectoryCopy()
500     {
501         return true;
502     }
503 
504     public void putDirectory( File sourceDirectory, String destinationDirectory )
505         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
506     {
507         if ( !sourceDirectory.isDirectory() )
508         {
509             throw new IllegalArgumentException( "Source is not a directory: " + sourceDirectory );
510         }
511 
512         putInternal( sourceDirectory, destinationDirectory );
513     }
514 
515     /**
516      * Check that the ScmResult was a successful operation
517      *
518      * @param result
519      * @throws TransferFailedException if result was not a successful operation
520      * @throws ScmException 
521      */
522     private void checkScmResult( ScmResult result )
523         throws ScmException
524     {
525         if ( !result.isSuccess() )
526         {
527             throw new ScmException( "Unable to commit file. " + result.getProviderMessage() + " "
528                 + ( result.getCommandOutput() == null ? "" : result.getCommandOutput() ) );
529         }
530     }
531 
532     public void closeConnection()
533         throws ConnectionException
534     {
535         removeCheckoutDirectory();
536     }
537 
538     /**
539      * Not implemented
540      *
541      * @throws UnsupportedOperationException always
542      */
543     public boolean getIfNewer( String resourceName, File destination, long timestamp )
544         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
545     {
546         throw new UnsupportedOperationException( "Not currently supported: getIfNewer" );
547     }
548 
549     public void get( String resourceName, File destination )
550         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
551     {
552         Resource resource = new Resource( resourceName );
553 
554         fireGetInitiated( resource, destination );
555 
556         String url = getRepository().getUrl() + "/" + resourceName;
557 
558         // remove the file
559         url = url.substring( 0, url.lastIndexOf( '/' ) );
560 
561         try
562         {
563             ScmRepository scmRepository = getScmRepository( url );
564 
565             fireGetStarted( resource, destination );
566 
567             // TODO: limitations:
568             // - destination filename must match that in the repository - should allow the "-d" CVS equiv to be passed
569             //   in
570             // - we don't get granular exceptions from SCM (ie, auth, not found)
571             // - need to make it non-recursive to save time
572             // - exists() check doesn't test if it is in SCM already
573 
574             File scmFile = new File( checkoutDirectory, resourceName );
575 
576             File basedir = scmFile.getParentFile();
577 
578             ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() );
579 
580             String reservedScmFile = scmProvider.getScmSpecificFilename();
581 
582             if ( reservedScmFile != null && new File( basedir, reservedScmFile ).exists() )
583             {
584                 scmProvider.update( scmRepository, new ScmFileSet( basedir ), (ScmVersion) null );
585             }
586             else
587             {
588                 // TODO: this should be checking out a full hierarchy (requires the -d equiv)
589                 basedir.mkdirs();
590 
591                 scmProvider.checkOut( scmRepository, new ScmFileSet( basedir ), (ScmVersion) null );
592             }
593 
594             if ( !scmFile.exists() )
595             {
596                 throw new ResourceDoesNotExistException( "Unable to find resource " + destination + " after checkout" );
597             }
598 
599             if ( !scmFile.equals( destination ) )
600             {
601                 FileUtils.copyFile( scmFile, destination );
602             }
603         }
604         catch ( ScmException e )
605         {
606             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
607             
608             throw new TransferFailedException( "Error getting file from SCM", e );
609         }
610         catch ( IOException e )
611         {
612             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
613             
614             throw new TransferFailedException( "Error getting file from SCM", e );
615         }
616 
617         postProcessListeners( resource, destination, TransferEvent.REQUEST_GET );
618 
619         fireGetCompleted( resource, destination );
620     }
621 
622     /**
623      * @return a List&lt;String&gt; with filenames/directories at the resourcepath.
624      * @see org.apache.maven.wagon.AbstractWagon#getFileList(java.lang.String)
625      */
626     public List getFileList( String resourcePath )
627         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
628     {
629         try
630         {
631             ScmRepository repository = getScmRepository( getRepository().getUrl() );
632 
633             ScmProvider provider = getScmProvider( repository.getProvider() );
634 
635             ListScmResult result = provider.list( repository,
636                                                   new ScmFileSet( new File( "." ), new File( resourcePath ) ), false,
637                                                   (ScmVersion) null );
638 
639             if ( !result.isSuccess() )
640             {
641                 throw new ResourceDoesNotExistException( result.getProviderMessage() );
642             }
643 
644             // List<String>
645             List files = new ArrayList();
646 
647             for ( Iterator it = result.getFiles().iterator(); it.hasNext(); )
648             {
649                 ScmFile f = (ScmFile) it.next();
650                 files.add( f.getPath() );
651                 System.out.println( "LIST FILE: " + f + " (path=" + f.getPath() + ")" );
652             }
653 
654             return files;
655         }
656         catch ( ScmException e )
657         {
658             throw new TransferFailedException( "Error getting filelist from SCM", e );
659         }
660     }
661 
662     public boolean resourceExists( String resourceName )
663         throws TransferFailedException, AuthorizationException
664     {
665         try
666         {
667             getFileList( resourceName );
668 
669             return true;
670         }
671         catch ( ResourceDoesNotExistException e )
672         {
673             return false;
674         }
675     }
676 
677     private String getFilename( String filename )
678     {
679         String fname = StringUtils.replace( filename, "/", File.separator );
680         return FileUtils.filename( fname );
681     }
682 
683     private String getDirname( String filename )
684     {
685         String fname = StringUtils.replace( filename, "/", File.separator );
686         return FileUtils.dirname( fname );
687     }
688 }